<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Paige Niedringhaus</title>
    <description>The latest articles on DEV Community by Paige Niedringhaus (@paigen11).</description>
    <link>https://dev.to/paigen11</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F463100%2Fdc77f76d-cdc9-43a6-9ec0-654dea7fd5bc.jpeg</url>
      <title>DEV Community: Paige Niedringhaus</title>
      <link>https://dev.to/paigen11</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paigen11"/>
    <language>en</language>
    <item>
      <title>Reviewing the BenQ RD Series Monitor for Developers</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Mon, 08 Sep 2025 12:46:00 +0000</pubDate>
      <link>https://dev.to/paigen11/reviewing-the-benq-rd-series-monitor-for-developers-1mho</link>
      <guid>https://dev.to/paigen11/reviewing-the-benq-rd-series-monitor-for-developers-1mho</guid>
      <description>&lt;h2&gt;
  
  
  I didn’t think developers needed a special monitor, until I tried the BenQ RD280U.
&lt;/h2&gt;

&lt;p&gt;Like many developers, I began working from home full time when the pandemic hit and the office I commuted to every day was forced to close. My initial WFH setup was at the dining room table with my 16" MacBook Pro, a leftover 18" Dell monitor as my second screen, and an old Microsoft wireless keyboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F69p31hcngx4pipr55owx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F69p31hcngx4pipr55owx.jpg" alt="Photo of initial, humble, work from home setup at the dining room table" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;v0 of the work from setup. It came from humble beginnings.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As the weeks turned into months and it became apparent no one would be going back to an office anytime soon, I began to slowly but steadily build a solid work from home station: laptop stand to raise my laptop to eye level, adjustable desk, extra office chair in the garage. &lt;/p&gt;

&lt;p&gt;Then mid-pandemic, I joined the fully remote tech startup I work for now, Blues, and it was game on: upgraded Logitech wireless keyboard, dual 27" HP monitors, upgraded office chair (with the rollerblade wheels, I might add), Thunderbolt docking station, external microphone, key light, and Elgato prompter (for the &lt;a href="https://front-end-fire.com/" rel="noopener noreferrer"&gt;weekly web dev podcast&lt;/a&gt; I started with my friends), and for quite a while I've felt very dialed in at this command station each day. After 5 years of incremental upgrades, you'd hope that would be the case.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F85l88c3ppq3ug94snuft.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F85l88c3ppq3ug94snuft.jpg" alt="Photo of upgraded work from home setup: adjustable desk, dual monitors, external microphone, key light, prompters" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The v1 wfh command station I've been perfecting for years now.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;About two months ago, out of the blue, the folks at tech product company BenQ reached out to me asking if I'd be interested in trying out one of its &lt;a href="https://www.benq.com/en-us/monitor/programming.html" rel="noopener noreferrer"&gt;&lt;strong&gt;RD series programming monitors&lt;/strong&gt;&lt;/a&gt;: a product line designed with developers' needs in mind. Having heard good things about BenQ, but never actually trying their products, I said "Sure!", and received a &lt;a href="https://benqurl.biz/4oHVx4E" rel="noopener noreferrer"&gt;&lt;strong&gt;28", 4K RD280U monitor&lt;/strong&gt;&lt;/a&gt; shipped to my front door about a week later.&lt;/p&gt;

&lt;p&gt;I was a little dubious some of the monitor's claims like coding-optimized color modes and auto dimming features for low-light environments were just marketing fluff, but once I tried it out, boy was I wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today, I'm going to review the &lt;a href="https://benqurl.biz/4oHVx4E" rel="noopener noreferrer"&gt;BenQ RD280U 28" 3:2 Monitor for Programming&lt;/a&gt;, and tell you how it's improved my day-to-day coding workflow in ways I never anticipated.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Initial assembly and setup of the RD280U was a breeze
&lt;/h2&gt;

&lt;p&gt;The RD280U ships in a what seems like large box, even for a 28" monitor, but all the pieces are well organized and protected, and my assembly of the monitor was done within minutes following the detailed illustrations in the Quick Start guide included in the box.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The box is quite large even though it's not actually that heavy. Based on its bulk, I would recommend you find a second person to help you get it inside and near to where you plan to assemble and use it, if possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The screen itself can tilt between -5˚ - 20˚  up and down, swivel right and left 15˚ , and adjust the height on its stand by 4.3" (110mm).&lt;/p&gt;

&lt;p&gt;It comes packaged with HDMI, USB-C, and USB-B cords for connecting to your laptop, and also has a display port as well, if that's your preferred connection method. One pleasant surprise for me was the lack of an external power pack that I'd have to secure somewhere behind my desk. Instead there's simply a power cord that runs from the monitor to an outlet.&lt;/p&gt;

&lt;p&gt;Additionally, there's a &lt;a href="https://www.benq.com/en-us/monitor/programming/rd280ua.html" rel="noopener noreferrer"&gt;flexible arm version of the monitor (the RD280UA)&lt;/a&gt; you can choose instead of the stand the RD280U comes with if you want to clamp it to your desk instead. &lt;/p&gt;

&lt;h2&gt;
  
  
  Noteworthy RD280U features for devs
&lt;/h2&gt;

&lt;p&gt;Ok, let's talk about the things that developers care most about: the monitor's features! The RD280U has a lot of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Coding Mode"
&lt;/h3&gt;

&lt;p&gt;The BenQ RD series monitors have a feature called "Coding Mode", which is a series of specialized display settings aimed at providing the best possible text clarity for programmers. Coding Mode adjusts settings to sharpen text and reduce eye strain, and has both Dark and Light themes depending on devs' user preferences. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm Team Dark Mode all day, by the way. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Below are more specifics on how Coding Mode enhances a typical coding session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Higher text clarity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This RD monitor has a max resolution of 4K+ UHD, which means four times more pixels and detail than a 1080p display. What this translates to in practice, is less eye strain when looking at code for hours on end. Coding Mode takes this a step further by fine-tuning the contrast, brightness, sharpness, saturation, and gamma settings levels for text, enhancing the overall sharpness and readability of code, even during long working sessions.&lt;/p&gt;

&lt;p&gt;Personally, I can see a difference when I compare two IDE windows side-by-side on my existing HP monitor vs the new BenQ monitor: the BenQ monitor &lt;em&gt;does&lt;/em&gt; make the code sharper and easier to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anti-reflection screen properties&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Until recently (when I was traveling for work and working from coffee shops and hotel cafes), I didn't think too much about screen glare and light reflection. It must just be where my office is set up at home, but it wasn't really an issue for me.&lt;/p&gt;

&lt;p&gt;Working remotely, however, screen glare was an issue I had to contend with and the BenQ Nano Matte Panel technology that actively minimizes contrast glare on the screen would have been most welcome. &lt;/p&gt;

&lt;p&gt;If you have lights set up behind you that cause reflections on your monitor or even just windows that let the sunlight in during a typical day, you'll probably really appreciate this feature as well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Function Bar&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An addition the RD monitors include that is very geared towards developers is the addition of a Function Bar at the bottom center of the monitor. &lt;/p&gt;

&lt;p&gt;It has a "Coding HotKey" on the front of the bar that looks like a closing HTML tag &amp;lt;/&amp;gt;, and when touched, it provides quick access to features like coding modes, screen brightness and contrast, and a customizable "Function Key" button on the bottom righthand side of the bar that can be set to different options you might want quick access to like Color Mode or input source. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5z4rsgde6n8bzxroz93u.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5z4rsgde6n8bzxroz93u.jpg" alt="Photo of the display panel when the Coding HotKey on the Function Bar is tapped" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Here's a shot of the settings the Coding HotKey displays when tapped on the front of the Function Bar.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In addition to the Function Key on the far right of the Function Bar, there's also a power button for the monitor and a center button that displays &lt;em&gt;all&lt;/em&gt; the RD monitor's settings (there's a lot of them). The settings are easy to navigate through as the buttons allow for left/right toggling through menus and the naming is also pretty intuitive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuxfrecrtq4b2fnxbn029.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuxfrecrtq4b2fnxbn029.jpg" alt="Photo of the full settings panel when the center button on the bottom of the Function Bar is pressed" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The full settings panel is accessed by pressing the center button on the bottom of the Function Bar.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The bar also has some LED indicators on the front next to the Coding HotKey to show which settings are currently on. &lt;/p&gt;

&lt;p&gt;It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Night Hours Protection indicator&lt;/strong&gt; (an owl head), which adjusts the screen to minimum brightness for coding in low light or at night, &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brightness Intelligence Gen2&lt;/strong&gt; (a polygon-esque lightbulb), that auto detects ambient light and adjusts the screen's brightness accordingly, &lt;/li&gt;
&lt;li&gt;And &lt;strong&gt;Low Blue Light Plus&lt;/strong&gt; (a lightbulb), which reduces blue light while preserving color quality. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For me, after toggling through my screen's settings on initial setup and ensuring my Color Mode theme was set to "Coding - Dark Theme", I haven't needed to use the Function Bar or Coding HotKey much since. &lt;/p&gt;

&lt;p&gt;I don't have multiple different computers I want to use this monitor with and switch between (although it supports that!), nor do I feel the need to change my Color Mode theme frequently, but for some devs this may be of greater value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MoonHalo backlight&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Another interesting feature I hadn't consciously considered but really like, is the backlight "MoonHalo" built into the RD280U. &lt;/p&gt;

&lt;p&gt;It's essentially a ring light built into the back of the monitor that can be set to turn on and provide ambient light while working. It can also auto dim to ultra low brightness with color-balance based on how dark your work surroundings are, which is influenced by the sensor in the Function Bar mentioned above that automatically detects and adjusts the display brightness based on the ambient brightness throughout the day. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The auto dimming feature, I appreciate a lot. It makes me feel less like I'm working in a cave at my desk after dark.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4gqcf251i9w51tm7diq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4gqcf251i9w51tm7diq.jpg" alt="An angled shot of the MoonHalo light on the back of the RD280U monitor" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Here's an angled shot of what the MoonHalo looks like on the back of the RD280U monitor. At this time of day, it's at a low brightness level.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While the MoonHalo certainly doesn't take the place of a key light if you do any video recording or streaming, it is nice for providing some extra ambient light and offering adjustable color temperature without needing a separate lamp, and doesn't do the "blinding white screen at night scenario" when the monitor screen is set for daytime brightness conditions by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional monitor features
&lt;/h3&gt;

&lt;p&gt;Those are some of the technologies and thoughtful additions that the RD series monitors offer specifically for programmers via Coding Mode, but there's some additional things worth pointing out in this review as well, that may just sell you on the BenQ RD monitor otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Display Pilot 2 software&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The BenQ line of monitors comes with its own software control app called &lt;a href="https://www.benq.com/en-us/monitor/software/display-pilot-2.html#RD%20models" rel="noopener noreferrer"&gt;Display Pilot 2&lt;/a&gt;, to let users efficiently control their display settings on their computer.&lt;/p&gt;

&lt;p&gt;The app offers a software interface for adjusting many of the settings I outlined above: audio levels, color mode, screen brightness, MoonHalo, switching screen orientation, and so on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Friunpderhrcpjnk5h8ik.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Friunpderhrcpjnk5h8ik.png" alt="Screenshot of Display Pilot 2 software interface" width="373" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Honestly, until I was writing this review, I hadn't even bothered to download the Display Pilot 2 software to my Mac, and while it provides a nice looking interface to control all the bells and whistles from my computer, I don't see myself using it often after I've dialed in my initial settings.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I should also note that while I was messing with the settings via Display Pilot 2, the software crashed a couple of times and caused my BenQ RD monitor to blink on and off more than once, so your mileage may vary when using it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Multi-source support for seamless source switching&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lot of developers have different rigs for different purposes: a gaming PC, a MacBook work, a personal laptop, or some other combination of devices.&lt;/p&gt;

&lt;p&gt;In the past, it's always been kind of a a pain to switch one machine for another to use the same peripherals (keyboard, external monitor, mouse, headphones, etc.).&lt;/p&gt;

&lt;p&gt;Well, the BenQ RD monitor can support more than one machine at a time. It has an HDMI port, display port, two USB-C ports, 1 USB-B port, 1 USB-A port, and a headphone jack. It also has a built-in KVM (keyboard, video, mouse) switch to make shifting from one machine to another easy.&lt;/p&gt;

&lt;p&gt;Simply connect Computer 1 and Computer 2 to the RD monitor, and both will be identified. Once both are recognized, you can switch the input video source from Computer 1 to Computer 2, and the monitor will also switch the USB signal to computer 2 as well. No muss, no fuss.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I have not tested this feature out myself, as I do not use multiple laptops with my current setup, but I trust the &lt;a href="https://www.benq.com/en-us/support/downloads-faq/faq/pre-sales/technologies/kvm-swich-to-work-with-one-keyboard-mouse-set.html" rel="noopener noreferrer"&gt;documentation the BenQ website provides about the KVM switch&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  My favorite monitor feature (I didn't see this one coming tbh)
&lt;/h3&gt;

&lt;p&gt;By far my favorite feature of this new RD monitor is (and I'm as shocked about this as anyone else considering all the cool tech I've covered up to now): its 3:2 aspect ratio design!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl61bl3nae22ogfyheoff.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl61bl3nae22ogfyheoff.jpg" alt="Photo of 3:2 screen aspect ratio of BenQ RD280U monitor" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Aspect ratio? Really?? That's what you're most jazzed about???&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 3:2 screen aspect ratio&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My friends, it is. &lt;/p&gt;

&lt;p&gt;I (apparently) am so used to the more standard 16:9 aspect ratio most monitors come with, that after a few days of adjusting to this new, bigger screen where I can see more lines of code or more info in my browser's dev tools &lt;em&gt;without having to scroll&lt;/em&gt;, I'm surprised no one really talks about how much of a difference it can make when evaluating external monitors. &lt;/p&gt;

&lt;p&gt;According to the &lt;a href="https://www.benq.com/en-us/knowledge-center/knowledge/boosting-programming-productivity-with-benq-right-aspect-ratio.html" rel="noopener noreferrer"&gt;BenQ website&lt;/a&gt;, this RD280U screen shows 7 more lines of code, but I tell you that for me it is quite a bit more than 7 lines, and I absolutely love it. And all this without me even flipping the monitor into a vertical orientation!&lt;/p&gt;

&lt;p&gt;I am very seriously considering getting another RD280U just so I can have this much extra screen real estate for both my external displays at home.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fusysakefn248dsw2zcdz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fusysakefn248dsw2zcdz.png" alt="Screenshot of amount of code visible on my old HP monitor vs amount of code visible on new RD monitor" width="800" height="642"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Just look at the amount of code visible on my old HP monitor (right side) vs the amount of code visible on the new RD monitor (left side). It's like night and day.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As humans, we adapt quickly to our current conditions until we get so used to it that they become "normal" to us. Another term for this is habituation.&lt;/p&gt;

&lt;p&gt;Over the years, I've developed what I consider a very effective and comfortable work from home setup: laptop stand, external monitors, keyboard and mouse, ergonomic desk chair, and even a small teleprompter screen, and I was quite content with it.&lt;/p&gt;

&lt;p&gt;Then, BenQ reached out to me with the chance to try one of its RD programming series monitors, and my eyes were opened to features I hadn't even considered I needed as a developer, but made the daily process of writing and reviewing code for hours so much nicer.&lt;/p&gt;

&lt;p&gt;A dedicated "Coding Mode" (with both dark and light modes) that enhances text clarity and contrast, anti-reflective screen, a backlight built in to the monitor itself, auto dimming software measuring ambient light, and a 3:2 aspect ratio that dramatically increased my screen real estate. &lt;/p&gt;

&lt;p&gt;None of these features were things even on my radar before the &lt;a href="https://benqurl.biz/4oHVx4E" rel="noopener noreferrer"&gt;BenQ RD280U&lt;/a&gt;, but now, I have a hard time imagining working without them.&lt;/p&gt;

&lt;p&gt;If you're considering an external monitor to enhance or upgrade your own coding practice, I would strongly encourage you to consider the BenQ RD280U or its other RD series monitors built specifically with developers and programmers in mind. I was skeptical at first that a monitor could really improve my day-to-day workflow, but I was wrong.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you'd like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com" rel="noopener noreferrer"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading, I hope you’ll check out the BenQ RD line of monitors the next time you need a new monitor. BenQ took the time to understand what improves the developer quality of life, and built a monitor with that really raises the bar with a ton of useful features.&lt;/p&gt;




&lt;h2&gt;
  
  
  References &amp;amp; Further Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.benq.com/en-us/monitor/programming.html" rel="noopener noreferrer"&gt;BenQ RD series programming monitors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://benqurl.biz/4oHVx4E" rel="noopener noreferrer"&gt;BenQ RD280U 28" 3:2 Monitor for Programming&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>benq</category>
      <category>review</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Build a Custom Time Slider Component with Ant Design and Next.js</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Fri, 24 Jan 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/paigen11/build-a-custom-time-slider-component-with-ant-design-and-nextjs-18c7</link>
      <guid>https://dev.to/paigen11/build-a-custom-time-slider-component-with-ant-design-and-nextjs-18c7</guid>
      <description>&lt;p&gt;&lt;a href="/static/61a294ed44476ee0aea97c7668fa1ad2/ae28e/hero.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F61a294ed44476ee0aea97c7668fa1ad2%2F1e043%2Fhero.png" title="Ant Design custom date range slider with differently styled future dates" alt="Ant Design custom date range slider with differently styled future dates" width="690" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Part of my job as a software engineer working for a startup involves building proof of concept (POC) web apps quickly, and one of my preferred tech stacks for such a project is a Next.js application in conjunction with the Ant Design component library.&lt;/p&gt;

&lt;p&gt;I'm a fan of &lt;a href="https://ant.design/" rel="noopener noreferrer"&gt;Ant Design&lt;/a&gt; because it's a library I'm familiar with, and it offers &lt;em&gt;a lot&lt;/em&gt; of more complicated components (think filterable tables, nested trees, complex forms, date pickers, and so on) that are highly customizable and just work right out of the box.&lt;/p&gt;

&lt;p&gt;When I was tasked recently with building a web app that contained a time slider akin to the adjustable sliders you see in interactive weather maps or other time series data applications that would allow a user to adjust the visible time range within a predetermined set of dates, because I couldn't find a suitable, React-based component or package already available, I ended up modifying a basic Ant Design slider component to suit my needs.&lt;/p&gt;

&lt;p&gt;My time slider component modifies its tick mark date formatting appropriately based on how big or small the original time span is, it allows users to drag the slider from either end to adjust the currently selected date range, it allows for date ranges set in the future, and it even delineates on the slider between past dates and future dates with different colors and styling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today, I'll show you how to build your own custom time slider inside of a Next.js application using the Ant Design  component and the Day.js date formatting library.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what the final adjustable time slider component looks like and a link to the &lt;a href="https://codesandbox.io/p/devbox/custom-time-slider-nextjs-ant-design-kcxm75" rel="noopener noreferrer"&gt;live demo site in CodeSandbox&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/LYYZ9lFvx90"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Install Ant Design and Day.js
&lt;/h2&gt;

&lt;p&gt;The two libraries you'll need to add to a React-based application to build this custom time range slider are the &lt;a href="https://ant.design/" rel="noopener noreferrer"&gt;&lt;strong&gt;Ant Design library&lt;/strong&gt;&lt;/a&gt; and the &lt;a href="https://day.js.org/" rel="noopener noreferrer"&gt;&lt;strong&gt;Day.js&lt;/strong&gt;&lt;/a&gt; library.&lt;/p&gt;

&lt;p&gt;Run this command in the terminal in the root of your project to add them both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;antd dayjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As I said before, Ant Design is a great, full-featured, very well documented component library I've worked with many times in the past and it makes building complex web apps that contain elements like drawers, cards, cascading selects, and a host of other components much quicker and easier to get going.&lt;/p&gt;

&lt;p&gt;Ant Design uses Day.js by default, and Day.js is a lightweight, JavaScript library that parses, validates, manipulates, and displays dates and times with a largely Moment.js-compatible API. If you've ever used Moment to handle dates in a JavaScript application, you'll feel right at home with Day.js, with the added bonus that it's a fraction of the size of Moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ant Design Slider
&lt;/h3&gt;

&lt;p&gt;One particularly useful component that Ant Design offers is a &lt;a href="https://ant.design/components/slider" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;Slider /&amp;gt;&lt;/code&gt;&lt;/a&gt; component with a lot of customizable functionality not many other sliders contain.&lt;/p&gt;

&lt;p&gt;For my app, I needed a slider that could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select a range within the slider's bound (instead of being anchored to one end of the slider or the other),&lt;/li&gt;
&lt;li&gt;Allow that range track to be dragged within the slider.&lt;/li&gt;
&lt;li&gt;Offer customizable tick marks, tooltips, and range track styles.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Day.js
&lt;/h3&gt;

&lt;p&gt;Working with dates in JavaScript applications is still quite the pain, even today without a supporting date library. The Day.js library is a faster, smaller alternative to Moment.js, an extremely popular library for handling dates and times, with a very similar API and great internationalization support.&lt;/p&gt;

&lt;p&gt;To make dates work for my app with the Ant Design slider, I needed a library that could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Transform human readable dates like "2024-12-01" into Unix millisecond timestamps, because the slider expects numbers (not date formatted objects) to be passed to it in order to work correctly&lt;/li&gt;
&lt;li&gt;Format those same dates appropriately for tick marks of varying date spans&lt;/li&gt;
&lt;li&gt;Format said date for hoverable tooltips as well.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Day.js library is capable of all this and more with simple, easy to understand functions and great documentation, and since it's the default library for Ant Design anyway, it make sense to just fall in line with what Ant D knows best.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a TimeSlider component
&lt;/h2&gt;

&lt;p&gt;Once those two libraries are installed in the project, it's time to move on to actually creating the &lt;code&gt;&amp;lt;TimeSlider /&amp;gt;&lt;/code&gt; component. Below is the starting code for the component. I'll go through all the pieces that comprise it afterwards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Slider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;antd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dayjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TimeSliderProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TimeSlider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;TimeSliderProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Col&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Slider&lt;/span&gt;
            &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;draggableTrack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
            &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="na"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
              &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Col&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Row&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TimeSlider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I created a new file called &lt;code&gt;TimeSlider.tsx&lt;/code&gt; where all this code is going to live, and made a basic shell of the component that accepts the props &lt;code&gt;outerDateRange&lt;/code&gt;, &lt;code&gt;innerDateRange&lt;/code&gt;, and &lt;code&gt;onChange()&lt;/code&gt; from the parent component. Later on in this article I'll show you how to set up the required state and functions in the parent component to pass down to this component.&lt;/p&gt;

&lt;p&gt;As this is actually a TypeScript file, this component also gets its own &lt;code&gt;TimeSliderProps&lt;/code&gt; types defined for each of the props being passed to it. This is where the &lt;code&gt;Day.js&lt;/code&gt; library initially comes into play as well, as the type for &lt;code&gt;outerDateRange&lt;/code&gt;, &lt;code&gt;innerDateRange&lt;/code&gt;, and even the &lt;code&gt;onChange()&lt;/code&gt; function expect Day.js dates to be passed to them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; All of my JavaScript-related code is actually written in TypeScript, so if you prefer to use JavaScript, just know that it can be fairly easily adapted to plain JS by removing things like types from the components and variable declarations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Inside of this component we're going to set a bunch of variables required by the Ant &lt;code&gt;&amp;lt;Slider /&amp;gt;&lt;/code&gt; component to render properly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;min&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; variable to tell the slider what it's beginning and ending values are.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;values&lt;/code&gt; object that is an array of the &lt;code&gt;innerDateRange&lt;/code&gt;'s &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;upto&lt;/code&gt; values.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; For the range slider to work as you'd expect with date objects, the dates must be converted to Unix Epoch time, that is why you'll notice both the &lt;code&gt;outerDateRange&lt;/code&gt; and &lt;code&gt;innerDateRange&lt;/code&gt; dates are being called with the Day.js library's &lt;a href="https://day.js.org/docs/en/display/unix-timestamp-milliseconds" rel="noopener noreferrer"&gt;&lt;code&gt;valueOf()&lt;/code&gt; function&lt;/a&gt;. This takes the date and turns it into a Unix Timestamp in milliseconds, which can then be passed to the &lt;code&gt;&amp;lt;Slider/&amp;gt;&lt;/code&gt; component.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the bare minimum of data the Ant Design component will need to actually render onscreen. Then, it's time to create the component with the help of the Ant Design &lt;code&gt;&amp;lt;Slider/&amp;gt;&lt;/code&gt; element and pass the necessary details to it.&lt;/p&gt;

&lt;p&gt;For a bit of nicer formatting, I also employed Ant Design's &lt;code&gt;&amp;lt;Col/&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;Row/&amp;gt;&lt;/code&gt; elements, but those are not required for this component to work. They help the component to lay out in a horizontal fashion and take up as much space as the screen width will allow.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;Slider/&amp;gt;&lt;/code&gt; component itself requires the &lt;code&gt;min&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; variables, the &lt;code&gt;values&lt;/code&gt; array, the &lt;code&gt;onChange()&lt;/code&gt; function, and in order to make it a range slider whose track can be dragged, the &lt;code&gt;range={{draggableTrack: true}}&lt;/code&gt; value must be added to the slider's props.&lt;/p&gt;

&lt;p&gt;If this component were to receive valid dates right now, it should render, albeit without much to see. Let's keep going.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/70005f5aa9fe1e5d7c3bc7d0a2572145/b54cd/basic-slider-1.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F70005f5aa9fe1e5d7c3bc7d0a2572145%2F1e043%2Fbasic-slider-1.png" title="Ant Design barebones custom date range slider" alt="Ant Design barebones custom date range slider" width="690" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Calculate and style the tick marks to handle different date ranges appropriately
&lt;/h3&gt;

&lt;p&gt;The next step is to calculate the tick marks that will appear along the slider's track showing the range of the slider.&lt;/p&gt;

&lt;p&gt;Here's the code that will get added to the &lt;code&gt;TimeSlider.tsx&lt;/code&gt; component to make that happen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Slider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;antd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SliderRangeProps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;antd/lib/slider&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dayjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TimeSliderProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TimeSlider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;TimeSliderProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tickCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tickInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;tickCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upto&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// Calculate time difference in milliseconds&lt;/span&gt;

    &lt;span class="c1"&gt;// Pick an appropriate level of granularity for the date range we're viewing&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hh:mm A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is less than a day, show hours and minutes&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;M/D, HH:mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is less than a week, show day of the week&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;M/D&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is less than a month, show month / day&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MMM D&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is less than a year, show month and day&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MMM YYYY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is more than a year, show year and month&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YYYY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is more than 10 years, show year&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;style&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CSSProperties&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// add ticks to marks&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tickInterval&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;tickInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Col&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Slider&lt;/span&gt;
            &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;draggableTrack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
            &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="na"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
              &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Col&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Row&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TimeSlider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First a couple of new variables are introduced at the top of the component: &lt;code&gt;tickCount&lt;/code&gt; and &lt;code&gt;tickInterval&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tickCount&lt;/code&gt; is the number of ticks that should be displayed on the slider.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tickInterval&lt;/code&gt; is a value calculated by determining how far away from each other the &lt;code&gt;min&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; dates are in time and then dividing that difference by the &lt;code&gt;tickCount&lt;/code&gt; number to figure out how to evenly space the ticks along the slider, regardless of what the actual date values are.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, I created a function called &lt;code&gt;fmt()&lt;/code&gt; which handles the heavy lifting of formatting the dates associated with the tick marks appropriately based on how far apart in time the dates actually are.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upto&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// Calculate time difference in milliseconds&lt;/span&gt;

    &lt;span class="c1"&gt;// Pick an appropriate level of granularity for the date range we're viewing&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hh:mm A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is less than a day, show hours and minutes&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;M/D, HH:mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is less than a week, show day of the week&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;M/D&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is less than a month, show month / day&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MMM D&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is less than a year, show month and day&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MMM YYYY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is more than a year, show year and month&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YYYY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// range is more than 10 years, show year&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;fmt()&lt;/code&gt; function takes in a date (which will eventually be a tick mark), translates it to a Day.js date (&lt;code&gt;d&lt;/code&gt;) that can be easily formatted as needed, figures out the range between between the &lt;code&gt;min&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; dates passed into the component from its parent, and then formats the &lt;code&gt;d&lt;/code&gt; date according to how wide the total timeline &lt;code&gt;range&lt;/code&gt; is.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;range&lt;/code&gt; is less than a day, the tick marks will be styled to show hours and minutes, if the &lt;code&gt;range&lt;/code&gt; is less than a week, it will show day of the week plus hours and minutes, and so on as the &lt;code&gt;range&lt;/code&gt; increases. It takes a little bit of effort to decide when it makes sense to switch from one tick display format to the next, but it ends up working out pretty well in practice. Feel free to adjust your formatting of different date ranges to suit your needs.&lt;/p&gt;

&lt;p&gt;Next I created the &lt;code&gt;marks&lt;/code&gt; that will get passed to the Ant Design slider component.&lt;/p&gt;

&lt;p&gt;These &lt;code&gt;marks&lt;/code&gt; are created in two parts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// marks for each end of the time slider component&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;style&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CSSProperties&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// add ticks to marks&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tickInterval&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;tickInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first half of the code, where &lt;code&gt;const marks&lt;/code&gt; is declared, is where the marks for each end of the slider are created. Each end's date is passed to the &lt;code&gt;fmt()&lt;/code&gt; function to have it correctly rendered based on the length of the time span.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, each tick between the two ends is run through the formatting function as well via this &lt;code&gt;for&lt;/code&gt; loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tickInterval&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;tickInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, the resulting &lt;code&gt;marks&lt;/code&gt; array is passed to the &lt;code&gt;&amp;lt;Slider/&amp;gt;&lt;/code&gt; component, along with all the other values covered earlier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Slider&lt;/span&gt;
&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;draggableTrack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="na"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="/static/fd1a47c0e17ec68d206318e4b27bb54c/26162/slider-ticks-2.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2Ffd1a47c0e17ec68d206318e4b27bb54c%2F1e043%2Fslider-ticks-2.png" title="Ant Design custom date range slider with formatted tick marks" alt="Ant Design custom date range slider with formatted tick marks" width="690" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Format the tooltip
&lt;/h3&gt;

&lt;p&gt;After marks for the timeline, it's time to format the tooltip when a user hovers over either end of the range slider. This is actually one of the easier bits of code within this component as well.&lt;/p&gt;

&lt;p&gt;Inside of the component under where all the variables are declared at the top add the following function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SliderRangeProps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;antd/lib/slider&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TimeSlider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;TimeSliderProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="c1"&gt;// variables declared up here&lt;/span&gt;

 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;formatter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;NonNullable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SliderRangeProps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tooltip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;formatter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MM/DD/YYYY HH:mm A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;formatter()&lt;/code&gt; function takes in the current value of the end of the range slider a user is hovering over, and reformats it to a nicely readable date.&lt;/p&gt;

&lt;p&gt;Then call this &lt;code&gt;formatter()&lt;/code&gt; function within the &lt;code&gt;&amp;lt;Slider/&amp;gt;&lt;/code&gt; component at the bottom of the component file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Slider&lt;/span&gt;
&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;draggableTrack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="na"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="nx"&gt;tooltip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;formatter&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Voila! Nicely formatted tooltips for the time range slider on hover.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/fee754709826a8231d2fe8191c678856/aea0a/slider-tooltip-3.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2Ffee754709826a8231d2fe8191c678856%2F1e043%2Fslider-tooltip-3.png" title="Ant Design custom date range slider with formatted tooltips on hover" alt="Ant Design custom date range slider with formatted tooltips on hover" width="690" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Style the track and marks to handle current and future dates differently
&lt;/h3&gt;

&lt;p&gt;Now for the extra credit additions to this time slider component: styling future dates differently within the same component. Sounds impossible? I thought so too, at first, but now I can assure you it's not.&lt;/p&gt;

&lt;p&gt;There's two changes I wanted to implement to help users know when they were looking at dates in the past/present versus dates in the future with the time slider:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I wanted to change the color of the track from Ant Design's light blue default to a different color, and&lt;/li&gt;
&lt;li&gt;I wanted to change change the color of tick marks in the future, as well.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Lucky for me, Ant Design allows for immense customization of its components, even down to things as specific as track and tick mark styling.&lt;/p&gt;

&lt;p&gt;The track styling was more complicated, so let's tackle that one first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dayjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TimeSlider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;outerDateRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;innerDateRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;TimeSliderProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// other variables declared at top of component here&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Calculate track styles based on the current date&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getTrackStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// All within past/present&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#91caff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// All in future&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rgb(165, 222, 129)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Split between past/present and future&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pastWidthPercentage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`linear-gradient(
            to right,
            #91caff 0%,
            #91caff &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pastWidthPercentage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%,
            rgb(165, 222, 129) &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pastWidthPercentage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%,
            rgb(165, 222, 129) 100%
          )`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Col&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Slider&lt;/span&gt;
            &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;draggableTrack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
            &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
              &lt;span class="na"&gt;track&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getTrackStyle&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;}}&lt;/span&gt;
            &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="na"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
              &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="p"&gt;}}&lt;/span&gt;
            &lt;span class="nx"&gt;tooltip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;formatter&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Col&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Row&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TimeSlider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to get the track to change color midway through (for when it straddled date ranges both in the past and the future), I needed to declare a new variable called &lt;code&gt;current&lt;/code&gt; to keep track of what the actual current date is.&lt;/p&gt;

&lt;p&gt;At the top of the component where the other variables are declared, &lt;code&gt;current&lt;/code&gt; is added to the mix, and it's simply calling the Day.js library's &lt;code&gt;valueOf()&lt;/code&gt; function) to get the current date as a Unix timestamp.&lt;/p&gt;

&lt;p&gt;Once the current date is identified, I made a function called &lt;code&gt;getTrackStyle()&lt;/code&gt; that compares the values captured by either end of the draggable date range slider and sets the color of the track accordingly.&lt;/p&gt;

&lt;p&gt;When both slider values are less than or equal to the current time, the entire track is styled light blue, when both slider values are greater than current time (i.e. the future), the entire track is styled light green. When the range spans both past and future dates, the function creates a gradient effect relying on the &lt;code&gt;pastWidthPercentage()&lt;/code&gt; function to determine where to transition the colors and the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient" rel="noopener noreferrer"&gt;CSS linear-gradient&lt;/a&gt; to create a split color effect.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'll be honest with you: ChatGPT helped me with this track styling magic. It's been extremely helpful for many complex things I've needed to do with Ant Design components, and I'd recommend working with it if you get stuck.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;getTrackStyle()&lt;/code&gt; function was passed to the &lt;code&gt;&amp;lt;Slider/&amp;gt;&lt;/code&gt; component inside its &lt;code&gt;styles&lt;/code&gt; property as &lt;code&gt;styles={{ track: getTrackStyle() }}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then, I styled the tick marks based in the future in a different color as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tickInterval&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;tickInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;futureDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;futureDate&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rgb(73, 163, 15)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To accomplish this, I modified the &lt;code&gt;for&lt;/code&gt; loop that went through all the predetermined ticks and compared each one to the &lt;code&gt;current&lt;/code&gt; variable declared above. If any of the marks were in the future, those marks got a little extra styling applied to them in the form of a green font color. If the marks were not in the future, they retained their original font color.&lt;/p&gt;

&lt;p&gt;Bonus section: I also wanted a "Now" tick mark to clearly delineate where the time slider track went from blue dates in the past/present to green dates in the future.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;style&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CSSProperties&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Now&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rgb(89, 147, 251)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;translate(-50%, -36px)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To add an extra tick mark entitled "Now" along my time slider track, I modified the &lt;code&gt;marks&lt;/code&gt; variable, adding in the &lt;code&gt;current&lt;/code&gt; variable as one more pre-defined tick mark, and giving it a label of "Now" and a little styling to stand apart from the rest of the tick marks along the track.&lt;/p&gt;

&lt;p&gt;The "Now" tick mark only shows up when the dates passed to the time slider actually cross the current date, but I think it aids in understanding of what's going on onscreen.&lt;/p&gt;

&lt;p&gt;With all of that done, the time slider component itself is complete, and it's time to add it to a parent component that will pass it the dates and functions it requires.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/67871c2d72787d65950d55fd7e542392/18d55/slider-future-date-styling-4.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F67871c2d72787d65950d55fd7e542392%2F1e043%2Fslider-future-date-styling-4.png" title="Ant Design custom date range slider with differently styled future dates" alt="Ant Design custom date range slider with differently styled future dates" width="690" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Import the TimeSlider component into the parent page
&lt;/h2&gt;

&lt;p&gt;Ok, now it's time to give the custom &lt;code&gt;&amp;lt;TimeSlider/&amp;gt;&lt;/code&gt; component the props it needs.&lt;/p&gt;

&lt;p&gt;In my case, I had other components that also needed to be aware of any changes to the date range for display purposes (I had chart and map child components that needed to be updated as well when selected dates changed), so I had a page within my Next.js app holding the date state variables and &lt;code&gt;onChange()&lt;/code&gt; function that were passed to the &lt;code&gt;&amp;lt;TimeSlider/&amp;gt;&lt;/code&gt; component.&lt;/p&gt;

&lt;p&gt;Inside of the &lt;code&gt;index.tsx&lt;/code&gt; file within the &lt;code&gt;pages/&lt;/code&gt; folder, I added the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dayjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;TimeSlider&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/TimeSlider&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/styles/MainContent.module.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;HomeProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endDate&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;HomeProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;initialDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setInitialDateRange&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;selectedDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSelectedDateRange&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleDateSliderChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;innerDateSpan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setSelectedDateRange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;innerDateSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;innerDateSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mainContent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h2&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;textAlign&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20px auto 0 auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nx"&gt;Custom&lt;/span&gt; &lt;span class="nx"&gt;Ant&lt;/span&gt; &lt;span class="nx"&gt;Design&lt;/span&gt; &lt;span class="nx"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt; &lt;span class="nx"&gt;Time&lt;/span&gt; &lt;span class="nx"&gt;Slider&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h2&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeSlider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TimeSlider&lt;/span&gt;
              &lt;span class="nx"&gt;outerDateRange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
                &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;initialDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;initialDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;}}&lt;/span&gt;
              &lt;span class="nx"&gt;innerDateRange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
                &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selectedDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selectedDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;}}&lt;/span&gt;
              &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleDateSliderChange&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/main&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getStaticProps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2024-12-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YYYY-MM-DDTHH:mm:ss[Z]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2025-02-28&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YYYY-MM-DDTHH:mm:ss[Z]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;index.tsx&lt;/code&gt; page, I created three variables related to the &lt;code&gt;&amp;lt;TimeSlider/&amp;gt;&lt;/code&gt; component.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;initialDate&lt;/code&gt; and &lt;code&gt;selectedDate&lt;/code&gt; React states, which are both objects containing a &lt;code&gt;startDate&lt;/code&gt; and &lt;code&gt;endDate&lt;/code&gt; property.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;handleDateSliderChange()&lt;/code&gt; function, which accepts new &lt;code&gt;startDate&lt;/code&gt; and &lt;code&gt;endDate&lt;/code&gt; values, and updates the &lt;code&gt;selectedDate&lt;/code&gt; state accordingly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For convenience, I chose to set my start and end date values inside the Next.js &lt;code&gt;getStaticProps()&lt;/code&gt; function call which runs before the page renders in the browser, but it's not required, and I set both date state objects equal to the defined dates.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;initialDate&lt;/code&gt; values (which end up determining the min and max ends of the time slider won't change again until they are modified in &lt;code&gt;getStaticProps()&lt;/code&gt; and the page is refreshed), but the &lt;code&gt;selectedDate&lt;/code&gt; values are free to change within those bounds thanks to the &lt;code&gt;handleDateSliderChange()&lt;/code&gt; function which will update the state of the &lt;code&gt;selectedDate&lt;/code&gt; values whenever the user interacts with the &lt;code&gt;&amp;lt;TimeSlider/&amp;gt;&lt;/code&gt; component.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;TimeSlider/&amp;gt;&lt;/code&gt; component was imported into the &lt;code&gt;index.tsx&lt;/code&gt; file and the &lt;code&gt;initialDate&lt;/code&gt; object was passed as its &lt;code&gt;outerDateRange&lt;/code&gt; prop, the &lt;code&gt;selectedDate&lt;/code&gt; object was passed as its &lt;code&gt;innerDateRange&lt;/code&gt; prop, and the &lt;code&gt;handleDateSliderChange()&lt;/code&gt; function was passed as its &lt;code&gt;onChange()&lt;/code&gt; prop.&lt;/p&gt;

&lt;p&gt;At this point, the component should have everything it needs to work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test out the Time Slider component
&lt;/h3&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/LYYZ9lFvx90"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;If you'd like to see a working demo of this &lt;code&gt;&amp;lt;TimeSlider /&amp;gt;&lt;/code&gt; component in action, I've got a &lt;a href="https://codesandbox.io/p/devbox/custom-time-slider-nextjs-ant-design-kcxm75?embed=1&amp;amp;file=%2Fsrc%2Fpages%2Findex.tsx" rel="noopener noreferrer"&gt;CodeSandbox demo&lt;/a&gt; for you to try out.&lt;/p&gt;

&lt;p&gt;Open a terminal and type &lt;code&gt;npm run dev&lt;/code&gt; to get it started.&lt;/p&gt;

&lt;p&gt;In addition to the time slider component, I also included a few nice extras: some Ant Design date picker components and buttons with values like "Last month", "Last week", and "Last day" to show other ways of manipulating the &lt;code&gt;selectedDate&lt;/code&gt; state controlled by the parent component.&lt;/p&gt;

&lt;p&gt;If you'd like to see how the time slider adjusts to larger or smaller date ranges or play around with past and future dates, just adjust the dates inside the &lt;code&gt;getStaticProps()&lt;/code&gt; function in the &lt;code&gt;index.tsx&lt;/code&gt; file and refresh the browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As I was building a React-based proof of concept web app that needed a time slider that users could drag and adjust to zoom in or out on particular pieces of data over time, I couldn't find a ready-made solution for such a need. So I built one myself.&lt;/p&gt;

&lt;p&gt;Using the Ant Design component library's highly customizable &lt;code&gt;&amp;lt;Slider/&amp;gt;&lt;/code&gt; component as the base and manipulating dates with the help of the Day.js library, I was able to build a flexible, full-featured, and decent looking time slider without a whole lot of extra code on my end.&lt;/p&gt;

&lt;p&gt;Then I took it a few steps further making the slider handle future dates and style them differently and even display a little "Now" tick mark when the slider crossed over dates in the past to dates in the future dates. It's a pretty specific use case, I know, but I think there's more possible applications for interesting sliders like this once you start to think beyond the typical date time options.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you’d like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com" rel="noopener noreferrer"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope if you need a way to control time series data views within an app and don't want to rely on just date pickers or inputs, you'll consider giving this solution a try. Enjoy!&lt;/p&gt;




&lt;h2&gt;
  
  
  References &amp;amp; Further Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ant.design/components/slider" rel="noopener noreferrer"&gt;Ant Design  component&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://day.js.org/" rel="noopener noreferrer"&gt;Day.js date time formatting library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codesandbox.io/p/devbox/custom-time-slider-nextjs-ant-design-kcxm75" rel="noopener noreferrer"&gt;CodeSandbox demo of working time slider component&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>typescript</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Automatically Publish a Repo as a PyPI Library with GitHub Actions</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Mon, 21 Oct 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/paigen11/automatically-publish-a-repo-as-a-pypi-library-with-github-actions-2dn1</link>
      <guid>https://dev.to/paigen11/automatically-publish-a-repo-as-a-pypi-library-with-github-actions-2dn1</guid>
      <description>&lt;p&gt;&lt;a href="/static/514ae96b68913d53775bdbff258a21a1/4b190/workflow-hero.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F514ae96b68913d53775bdbff258a21a1%2F15ec7%2Fworkflow-hero.jpg" title="Person diagramming out a mobile app workflow" alt="Person diagramming out a mobile app workflow" width="690" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Last year I had the opportunity to turn my company's API into a JavaScript SDK by relying on the project's &lt;code&gt;openapi.yaml&lt;/code&gt; file and the &lt;a href="https://openapi-generator.tech/docs/installation" rel="noopener noreferrer"&gt;&lt;strong&gt;OpenAPI Generator CLI&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Notehub JS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the full details of how I did created Notehub JS, I encourage you to read the original blog post I published about it &lt;a href="https://www.paigeniedringhaus.com/blog/use-git-hub-actions-to-automatically-publish-a-repo-subfolder-as-an-npm-library" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I did this as much for myself as for other developers, because I and a group of my coworkers were building a lot of JavaScript-based web apps to display and interact with the IoT data our company &lt;a href="https://blues.com/" rel="noopener noreferrer"&gt;Blues&lt;/a&gt; specializes in transporting from a device in the real world to the cloud via cellular. To make it easier to update the JavaScript library as the API it's based on continues to grow and evolve, I set up a bunch of GitHub Actions workflows to do most of the tedious, repetitive tasks for me, and I learned a bunch of useful new things along the way.&lt;/p&gt;

&lt;p&gt;This year, one of my other coworkers who works frequently in Python, asked if I could create a Python SDK for the API, and I agreed, feeling pretty confident about most of the steps involved. One new thing I did have to learn was how to build and publish a Python package to the Python Package Index, PyPI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In this blog, I'll show you how to set up a GitHub Actions workflow to automatically publish a new version of a GitHub project to PyPI when a new release is made - no muss, no fuss, very little manual input required.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Notehub Py
&lt;/h2&gt;

&lt;p&gt;As with the original Notehub JS project, the &lt;a href="https://github.com/blues/notehub-py" rel="noopener noreferrer"&gt;&lt;strong&gt;Notehub Py&lt;/strong&gt;&lt;/a&gt; project's structure is a bit different from your typical repo because it is automatically generated from the &lt;a href="https://dev.blues.io/reference/notehub-api/api-introduction/" rel="noopener noreferrer"&gt;Notehub API's&lt;/a&gt; &lt;code&gt;openapi.yaml&lt;/code&gt; file. The &lt;code&gt;openapi.yaml&lt;/code&gt; file follows the &lt;a href="https://swagger.io/specification/" rel="noopener noreferrer"&gt;OpenAPI specification&lt;/a&gt; standards, and can be used with the &lt;a href="https://openapi-generator.tech/docs/installation" rel="noopener noreferrer"&gt;&lt;strong&gt;OpenAPI Generator CLI&lt;/strong&gt;&lt;/a&gt; to build a Python-based SDK to interact with the Notehub API in just a few commands from a terminal.&lt;/p&gt;

&lt;p&gt;Here's a simplified view of the &lt;a href="https://github.com/blues/notehub-py" rel="noopener noreferrer"&gt;Notehub Py repo&lt;/a&gt;'s folder structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── .github/
│ └── workflows/
│ └── GH Action files
├── lib_template/
│ └── python library template files
├── src/
│ ├── notehub_py/
│ │ └── Python-based API and model files
│ ├── docs/
│ │ └── MD documentation
│ ├── test/
│ │ └── unit tests
│ ├── dist/
│ │ └── bundled .tar and .whl binaries for PyPi
│ ├── pyproject.toml
│ ├── requirements.txt
│ └── setup.py
├── openapi.yaml
├── config.json
├── README.md
└── scripts.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;openapi.yaml&lt;/code&gt; file lives at the root level of the project along with its &lt;code&gt;config.json&lt;/code&gt; file, a &lt;code&gt;scripts.py&lt;/code&gt; file, and a few other other bits and pieces (license, contribution guidelines, code of conduct, etc.), but the real meat of the library lives inside of the &lt;code&gt;src/&lt;/code&gt; subfolder.&lt;/p&gt;

&lt;p&gt;What's unique about this subfolder is that it is regenerated each time the &lt;code&gt;openapi.yaml&lt;/code&gt; file is updated, and it has all the API endpoints, models, docs, and the &lt;code&gt;dist/&lt;/code&gt; folder for the &lt;code&gt;notehub-py&lt;/code&gt; SDK that actually gets published on PyPI. Publishing just a subfolder of a project inside a GitHub repo is a little unusual, but not to worry, it can be done in an automated fashion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a &lt;code&gt;scripts.py&lt;/code&gt; and &lt;code&gt;config.json&lt;/code&gt; file to automate generating the library and packaging it for distro to PyPI
&lt;/h3&gt;

&lt;p&gt;To automate the build and publish steps to deploy Notehub Py to the PyPI registry, we need a couple of things: a &lt;code&gt;config.json&lt;/code&gt; file and a &lt;code&gt;scripts.py&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;config.json&lt;/code&gt; file is a configuration file of additional properties used by the OpenAPI Generator and its Python library template to define certain variables like package name, package version, GitHub repo URL, etc. Here is what the Notehub Py's &lt;code&gt;config.json&lt;/code&gt; file looks like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/blues/notehub-py/blob/main/config.json" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;config.json&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packageName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notehub_py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packageUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/blues/notehub-py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"projectName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notehub-py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packageVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.2"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time a new version of the Notehub Py library needs to be published to PyPI, the &lt;code&gt;packageVersion&lt;/code&gt; for the project will be updated in this file, then the commands to regenerate the library and its distribution packages (which I'll cover next) gets run.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;scripts.py&lt;/code&gt; file is a set of reusable commands to automate the steps of updating this repo based on the latest version of the &lt;code&gt;openapi.yaml&lt;/code&gt;, and packaging it up for publishing to PyPI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/blues/notehub-py/blob/main/scripts.py" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;scripts.py&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_package&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openapi-generator-cli&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--library&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urllib3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lib_template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-i&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openapi.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Exception when generating package: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_distro_package&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Check if the 'dist/' folder exists
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# If it exists, delete it and its contents
&lt;/span&gt;            &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rmtree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Upgrade the 'build' module
&lt;/span&gt;        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;install&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--upgrade&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="c1"&gt;# Generate a new 'dist/' folder  
&lt;/span&gt;        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;])&lt;/span&gt;  
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Exception when building distro package: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Usage: python3 scripts.py [generate_package | build_distro_package]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;script_to_run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;script_to_run&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generate_package&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;generate_package&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;script_to_run&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build_distro_package&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;build_distro_package&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid script name. Use one of: generate_package, build_distro_package&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first function, &lt;code&gt;generate_package()&lt;/code&gt;, uses the &lt;code&gt;subprocess&lt;/code&gt; module to run the &lt;code&gt;openapi-generator-cli&lt;/code&gt; tool to generate a new version of the Notehub Py library. Let's break down each of the arguments in the command:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"generate"&lt;/code&gt;: Specifies the action to generate code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"-g", "python"&lt;/code&gt;: Indicates that the target language for the generated code is Python.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"--library", "urllib3"&lt;/code&gt;: Specifies that the generated code should use the &lt;code&gt;urllib3&lt;/code&gt; library for HTTP requests.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"-t", "lib_template"&lt;/code&gt;: Points to a template directory named &lt;code&gt;lib_template&lt;/code&gt; that contains custom templates for the code generation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"-o", "src"&lt;/code&gt;: Sets the output directory to &lt;code&gt;src&lt;/code&gt;, where the generated code will be placed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"-i", "openapi.yaml"&lt;/code&gt;: Specifies the input OpenAPI specification file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"-c", "config.json"&lt;/code&gt;: Uses a configuration file named &lt;code&gt;config.json&lt;/code&gt; to customize the generation process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second function, &lt;code&gt;build_distro_package()&lt;/code&gt;, builds the distribution package for the the Notehub Py project that will be deployed to PyPI.&lt;/p&gt;

&lt;p&gt;First, the function checks if a &lt;code&gt;dist&lt;/code&gt; folder exists inside the &lt;code&gt;src&lt;/code&gt; folder where all the Notehub Py API code lives, and deletes the older version of the folder if it does exist to ensure any previous build artifacts are removed before adding a new distro package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Check if the 'dist/' folder exists
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# If it exists, delete it and its contents
&lt;/span&gt;    &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rmtree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, the function upgrades the &lt;code&gt;build&lt;/code&gt; module to ensure the latest version is installed. This module is essential for generating the distribution packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;install&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--upgrade&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, the function generates a new &lt;code&gt;dist&lt;/code&gt; directory by invoking the &lt;code&gt;build&lt;/code&gt; module to create the distribution package, which includes both source distributions and wheel distributions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both of these functions can be invoked from the command line by running &lt;code&gt;python scripts.py generate_package&lt;/code&gt; or &lt;code&gt;python scripts.py build_distro_package&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up a PyPI account and library to publish to
&lt;/h3&gt;

&lt;p&gt;Now that the Notehub Py config file and scripts to automate the generation and building of the SDK are done, it's time to prepare PyPI to receive the library.&lt;/p&gt;

&lt;p&gt;Follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/account/register/" rel="noopener noreferrer"&gt;Create a PyPI user account&lt;/a&gt; if you haven’t done so already.&lt;/li&gt;
&lt;li&gt;After logging into PyPI, go to the “Account Settings” page and select the “Publishing” tab.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="/static/a54270eecce429beb2a0f63786ca078e/33c9c/pypi-acct-settings.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2Fa54270eecce429beb2a0f63786ca078e%2F1e043%2Fpypi-acct-settings.png" title="Navigating to the publishing tab in PyPI site" alt="Navigating to the publishing tab in PyPI site" width="690" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get to this screen by clicking the “Account Settings” tab in the user’s dropdown menu, then select “Publishing” under “Your account”.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scroll down to the “Add a new pending publisher” section of the page, and input the details of your project you want to publish to PyPI: publisher platform (GitHub), project name to display on PyPI, project owner, repo name, publishing workflow name (something like &lt;code&gt;publish-pypi.yaml&lt;/code&gt;), and click the "Add" button when you're done.

&lt;ol&gt;
&lt;li&gt;Using &lt;a href="https://docs.pypi.org/trusted-publishers/" rel="noopener noreferrer"&gt;PyPI’s trusted publishing option&lt;/a&gt; utilizes OpenID Connect (OIDC) technology to provide credential-free publishing authority to trusted third party services like GitHub Actions. This allows us to automate the release process without needing to use API tokens or passwords.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;Now, we're ready for the final step in the process: a GitHub Actions workflow to automate publishing the newly generated distribution packages to PyPI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write a GitHub Actions workflow to publish to PyPI
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Need a refresher on GitHub Actions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want a quick primer on what GitHub Actions are, I recommend you check out a previous article I wrote about them &lt;a href="https://www.paigeniedringhaus.com/blog/use-secret-environment-variables-in-git-hub-actions" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For me, it made the most sense to trigger a GitHub Actions workflow by creating a new &lt;a href="https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases" rel="noopener noreferrer"&gt;&lt;strong&gt;release&lt;/strong&gt;&lt;/a&gt; in GitHub to publish the Notehub Py &lt;code&gt;src/&lt;/code&gt; subfolder to PyPI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;GitHub specifically defines a &lt;strong&gt;release&lt;/strong&gt; as a deployable software iteration that you can package and make available for a wider audience to download and use, which is exactly what I want.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once I'd chosen this as the trigger for my workflow, it was a pretty straightforward set of steps to publish to PyPI.&lt;/p&gt;

&lt;p&gt;Here's what the finished &lt;code&gt;publish-pypi.yml&lt;/code&gt; file looks like inside of the &lt;code&gt;./github/workflows/&lt;/code&gt; folder - I'll break it all down below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/blues/notehub-py/tree/main/.github/workflows" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;publish-pypi.yml&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Upload Python Package

on:
  release:
    types: [created]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: 
      name: pypi
      url: https://pypi.org/p/notehub-py
    permissions:
      id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
    defaults:
      run:
        working-directory: ./src
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.x"

      - name: Install dependencies
        run: |
          python3 -m pip install --upgrade pip
          pip install build

      - name: Build package
        run: |
          python3 -m pip install --upgrade build
          python3 -m build

      - name: Publish package to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          packages-dir: ./src/dist/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each GH Actions workflow file needs a &lt;strong&gt;&lt;code&gt;name&lt;/code&gt;&lt;/strong&gt; , so I chose: &lt;code&gt;Upload Python Package&lt;/code&gt;. It tells users exactly what this script's purpose is.&lt;/p&gt;

&lt;p&gt;As I said earlier, this workflow is triggered whenever a new release is created in GitHub, which is where the following lines take effect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;on:
  release: 
    types: [created]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;on&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is how a workflow is triggered.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;release&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is the event that triggers the workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;types: [created]&lt;/code&gt;&lt;/strong&gt; is the activity type for a &lt;code&gt;release&lt;/code&gt; event that triggers the workflow. This gives us more fine-grained control of when the workflow should run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the &lt;strong&gt;&lt;code&gt;jobs&lt;/code&gt;&lt;/strong&gt; section runs inside of the workflow. This particular script only has one job, &lt;code&gt;deploy&lt;/code&gt;, but if there's multiple jobs, they'll run sequentially unless otherwise specified.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;deploy&lt;/code&gt; job defines that it runs on the latest version of Ubuntu in &lt;strong&gt;&lt;code&gt;runs-on&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The job is configured to operate within an environment named &lt;code&gt;pypi&lt;/code&gt;, with a URL that points to the PyPI project page for &lt;code&gt;notehub-py&lt;/code&gt;, and the &lt;code&gt;permissions&lt;/code&gt; section grants the job the necessary &lt;code&gt;id-token: write&lt;/code&gt; permission, which is required for trusted publishing to PyPI. All of this will correspond to the details filled out in PyPI in the previous section setting up the &lt;a href="https://docs.pypi.org/trusted-publishers/" rel="noopener noreferrer"&gt;trusted publishing option via OpenID Connect&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; environment: 
      name: pypi
      url: https://pypi.org/p/notehub-py
    permissions:
      id-token: write # this permission is mandatory for trusted publishing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defaultsrun" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;defaults.run&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; section sets the working directory to &lt;code&gt;./src&lt;/code&gt; for all &lt;code&gt;run&lt;/code&gt; steps, ensuring the commands are executed within the source subdirectory of the project.&lt;/p&gt;

&lt;p&gt;Finally, we get to the &lt;strong&gt;&lt;code&gt;steps&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The steps are as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check out the code so the workflow can clone the repository's code into the runner with &lt;a href="https://github.com/actions/checkout" rel="noopener noreferrer"&gt;&lt;code&gt;actions/checkout@v4&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Set up a Python environment with version &lt;code&gt;3.x&lt;/code&gt; using &lt;a href="https://github.com/actions/setup-python" rel="noopener noreferrer"&gt;&lt;code&gt;actions/setup-python@v5&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Upgrade &lt;code&gt;pip&lt;/code&gt; and install the &lt;code&gt;build&lt;/code&gt; modules necessary for building the distribution package.&lt;/li&gt;
&lt;li&gt;Build the distribution package using &lt;code&gt;python3 -m build&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Publish the built packages to PyPI using the &lt;a href="https://github.com/pypa/gh-action-pypi-publish" rel="noopener noreferrer"&gt;&lt;code&gt;pypa/gh-action-pypi-publish@release/v1&lt;/code&gt;&lt;/a&gt; action. The &lt;code&gt;packages-dir&lt;/code&gt; parameter specifies the directory containing the distribution files (&lt;code&gt;./src/dist/&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And there you have it: each time a new release is created in the Notehub Py repo, this GitHub Actions workflow will run and deploy the updated code to PyPI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;After publishing my first JavaScript SDK of my company's API on npm last year, I was asked to build a similar SDK in Python and distribute it to PyPI.&lt;/p&gt;

&lt;p&gt;I was able to repurpose a lot of the same steps and workflows I used for generating the JavaScript library to generate the Python library, but configuring the deployment to PyPI was a bit different as that package platform recommends using trusted publishing to deploy new package versions.&lt;/p&gt;

&lt;p&gt;With just a few Python scripts, a config file, and a little bit of set up on the PyPI site, I was able to deploy the auto-generated subfolder inside of the Notehub Py project to PyPI, helping developers more easily interact with the Notehub API. GitHub Actions allowed me to build the code, package it up for distribution, and publish it to PyPI in no time.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more about the useful things I learned while building this project in addition to other topics on JavaScript, React, IoT, or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you’d like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com" rel="noopener noreferrer"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope learning how to deploy a subfolder of a project to PyPI through GitHub Actions workflows comes in handy for you in the future. Enjoy!&lt;/p&gt;




&lt;h2&gt;
  
  
  References &amp;amp; Further Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/blues/notehub-py" rel="noopener noreferrer"&gt;Notehub Py&lt;/a&gt; GitHub repo&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/notehub-py/" rel="noopener noreferrer"&gt;notehub-py&lt;/a&gt; SDK on PyPI&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openapi-generator.tech/docs/installation" rel="noopener noreferrer"&gt;OpenAPI Generator CLI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>github</category>
      <category>git</category>
      <category>pypi</category>
    </item>
    <item>
      <title>Animate an Auto-Scrolling Carousel with Only HTML and CSS</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Fri, 23 Aug 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/paigen11/animate-an-auto-scrolling-carousel-with-only-html-and-css-4h8o</link>
      <guid>https://dev.to/paigen11/animate-an-auto-scrolling-carousel-with-only-html-and-css-4h8o</guid>
      <description>&lt;p&gt;&lt;a href="/static/5213fce8f8f6dbe59abd384581999936/5a190/sample-web-app-hero.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F5213fce8f8f6dbe59abd384581999936%2F1e043%2Fsample-web-app-hero.png" title="Cellular-connected electronic kiosk demo app showing weather forecast in Barcelona" alt="Cellular-connected electronic kiosk demo app showing weather forecast in Barcelona" width="690" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As web developers working with powerful computers we don't often stop to consider that while things like memory and stable Internet connections aren't a problem for us, they may be where our apps end up running.&lt;/p&gt;

&lt;p&gt;Case in point: building a web app to run on a Raspberry Pi - which could be quite possible for something like a kiosk in a mall, a doctor's office or even an amusement park.&lt;/p&gt;

&lt;p&gt;Last year, I was tasked with building a small demo for work to demonstrate how to package up a simple web application and download it to a Raspberry Pi using a cellular-based Blues Notecard.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you're interested in how to do this, you can read the full details &lt;a href="https://github.com/blues/accelerators-cellular-connected-electronic-kiosk/tree/main/web-app" rel="noopener noreferrer"&gt;here&lt;/a&gt;. But that is outside the scope of this post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I decided a good demo would be a weather app where a list of cities would be provided, and the app would fetch the weather forecast for each city and display it in an endlessly looping carousel.&lt;/p&gt;

&lt;p&gt;In order to keep the app size small (and quicker to download via cellular data to a Raspi), I built it with pure HTML, CSS, and a little vanilla-JavaScript.&lt;/p&gt;

&lt;p&gt;For the auto-scrolling feature of each city's forecast I'd normally look for a carousel library to help me, but since I needed the app to stay lightweight I did some research and found that CSS alone could do what I needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/11d1c85b08ea41cc4c6f68570f092681/d3be9/huzzah-gift.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F11d1c85b08ea41cc4c6f68570f092681%2Fd3be9%2Fhuzzah-gift.webp" title="Two Englishmen saying 'Huzzah'" alt="Two Englishmen saying 'Huzzah'" width="480" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today, I'll show you how to use CSS &lt;code&gt;keyframes&lt;/code&gt; animations along with scroll snap module and a few well-placed CSS classes to make auto-scrolling, CSS-only carousels possible.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Here's what the final auto-scrolling carousel animation looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS keyframes and scroll snap
&lt;/h2&gt;

&lt;p&gt;Before we get to the implementation of this auto-scrolling carousel, let's talk a bit about the CSS features that make it possible: CSS &lt;code&gt;keyframes&lt;/code&gt; and the CSS scroll snap module.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS keyframes
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes" rel="noopener noreferrer"&gt;&lt;code&gt;@keyframes&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; rule controls the intermediate steps in a CSS animation sequence by defining styles for keyframes (or waypoints) along an animation sequence. This gives more control over the intermediate steps of the animation sequence than CSS transitions.&lt;/p&gt;

&lt;p&gt;Not only do &lt;code&gt;@keyframes&lt;/code&gt; allow for complex animations without the need for JavaScript, but they also offer a straightforward syntax, and reusability across multiple elements (as you'll soon see in my own code examples below).&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS scroll snap
&lt;/h3&gt;

&lt;p&gt;The CSS &lt;strong&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll_snap" rel="noopener noreferrer"&gt;scroll snap module&lt;/a&gt;&lt;/strong&gt; provides properties that let you control the panning and scrolling behavior by defining snap positions. Content can be snapped into position as the user scrolls overflowing content within a scroll container, providing paging and scroll positioning.&lt;/p&gt;

&lt;p&gt;It has two properties in particular we'll take advantage of today for the carousel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSS scroll-snap-type&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The CSS &lt;strong&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type" rel="noopener noreferrer"&gt;&lt;code&gt;scroll-snap-type&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; property sets how strictly snap points are enforced on the scroll container.&lt;/p&gt;

&lt;p&gt;It accepts a variety of different &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type#syntax" rel="noopener noreferrer"&gt;values&lt;/a&gt;, but the most common ones are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mandatory&lt;/code&gt; - The scroll container must snap to the nearest snap point after scrolling.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;proximity&lt;/code&gt; - The scroll container will only snap to a point if the scrolling comes to a natural rest near a snap point.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;none&lt;/code&gt; - No snapping behavior is enforced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, you might use &lt;code&gt;mandatory&lt;/code&gt; when you need precise control over the final position of the scroll (like with carousels or paginated content where each item should be fully visible after scrolling), and you might use &lt;code&gt;proximity&lt;/code&gt; when you want to give users more control over the scroll stopping point, which can feel smoother and less jarring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSS scroll-snap-align&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The other part of this scroll snap equation, &lt;strong&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-align" rel="noopener noreferrer"&gt;&lt;code&gt;scroll-snap-align&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;, specifies a box's snap position within its snap container when using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll_snap" rel="noopener noreferrer"&gt;CSS scroll snap&lt;/a&gt; module.&lt;/p&gt;

&lt;p&gt;A child element of the scroll snap container can have the &lt;code&gt;scroll-snap-align&lt;/code&gt; property set to control how it snaps into the viewport when scrolling stops. Its &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-align#values" rel="noopener noreferrer"&gt;values&lt;/a&gt; include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;start&lt;/code&gt; - Aligns the start edge of the item with the start edge of the scroll container.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;end&lt;/code&gt; - Aligns the end edge of the item with the end edge of the scroll container.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;center&lt;/code&gt; - Centers the item in the scroll container.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;none&lt;/code&gt; - No snap alignment will occur.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that bit of background out of the way, let's get on to the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up the HTML and CSS
&lt;/h2&gt;

&lt;p&gt;First, the proper HTML must be set up for the CSS auto-scrolling to work correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Wrap the HTML elements to be part of the carousel in a parent div.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To begin, wrap all the slides to be scrolled through inside a parent container. This is where the CSS &lt;code&gt;scroll-snap-type&lt;/code&gt; property will be applied so that the child elements will be able to interpret the &lt;code&gt;scroll-snap-align&lt;/code&gt; property applied to them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; In my full code sample, I defined an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template" rel="noopener noreferrer"&gt;HTML template&lt;/a&gt; to hold all my weather data for each city, and the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template#examples" rel="noopener noreferrer"&gt;&lt;code&gt;template.content.cloneNode()&lt;/code&gt; function&lt;/a&gt; to clone the template for each city in my list and add it inside the &lt;code&gt;weather-forecast-container&lt;/code&gt; element&lt;/p&gt;

&lt;p&gt;That's outside the scope of what's needed for this article, but if you'd like to see the full code, it's available &lt;a href="https://github.com/blues/accelerators-cellular-connected-electronic-kiosk/blob/main/web-app/index.htm" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is my HTML file where the parent container and its children are defined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/blues/accelerators-cellular-connected-electronic-kiosk/blob/main/web-app/index.htm" rel="noopener noreferrer"&gt;&lt;code&gt;index.htm&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"weather-forecast-container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"weather-template"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"current-header-wrapper"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"current-date"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"location"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Loading current weather...&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"current-conditions"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"conditions"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"condition-icon"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"condition-text"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"current-temp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"details"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wind-speed"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"rainfall"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pressure"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"forecast"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;table&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;tbody&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"future-forecast"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"weather-carousel-snapper"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see from the HTML above, the &lt;code&gt;weather-forecast-container&lt;/code&gt; class wraps the div with the class of &lt;code&gt;weather-template&lt;/code&gt; which holds all the weather info for a given city, and contains an empty div at the bottom with a class of &lt;code&gt;weather-carousel-snapper&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Position the empty div at the bottom of the &lt;code&gt;weather-template&lt;/code&gt; div to be the carousel's snapper element.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Make sure that this element is &lt;em&gt;inside&lt;/em&gt; of the &lt;code&gt;weather-template&lt;/code&gt; div and not a sibling to it. It will continue to be empty, but it needs to be present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"weather-carousel-snapper"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Add base styling to the parent container and position the weather template and scroll-snap div inside it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where the CSS scroll snap module comes into play.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/blues/accelerators-cellular-connected-electronic-kiosk/blob/main/web-app/styles.css" rel="noopener noreferrer"&gt;&lt;code&gt;styles.css&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.weather-forecast-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;290px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;88vh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;430px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;780px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scroll-snap-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="n"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scroll-behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;smooth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;scroll&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the &lt;code&gt;weather-forecast-container&lt;/code&gt; class, we're going to go beyond the basic styling of the container like positioning and defining its &lt;code&gt;height&lt;/code&gt; and &lt;code&gt;width&lt;/code&gt; (both set to the width of a small screen attached to the Raspi), and add the first CSS scroll snap properties here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type#values" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;scroll-snap-type: x mandatory;&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; - this defines a mandatory snapping in the horizontal axis.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;scroll-behavior: smooth;&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; - makes the carousel appear to scroll to the next page, instead of just jumping there as it would by default.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-x#scroll" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;overflow-x: scroll;&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; - overflow content is clipped if necessary to fit horizontally inside the element's padding box.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up, is the styling for the &lt;code&gt;weather-template&lt;/code&gt; div that contains the weather info and the empty &lt;code&gt;weather-carousel-snapper&lt;/code&gt; div.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/blues/accelerators-cellular-connected-electronic-kiosk/blob/main/web-app/styles.css" rel="noopener noreferrer"&gt;&lt;code&gt;styles.css&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.weather-template&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;780px&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-repeat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;no-repeat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;space-evenly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've included the stying for the &lt;code&gt;weather-template&lt;/code&gt; div as it's the true parent of the &lt;code&gt;weather-carousel-snapper&lt;/code&gt; element, but this is just standard CSS styling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/blues/accelerators-cellular-connected-electronic-kiosk/blob/main/web-app/styles.css" rel="noopener noreferrer"&gt;&lt;code&gt;styles.css&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.weather-carousel-snapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last but not least, we add the last bit of scroll snap CSS to the empty div with the class of &lt;code&gt;weather-carousel-snapper&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-align#center" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;scroll-snap-align: center;&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; - this will align each item to the center of the container.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Create the animation keyframes to auto-scroll the forecast slides&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After the CSS styling is done, and scroll snap is enabled, it's time to make the &lt;code&gt;@keyframes&lt;/code&gt; animations that will actually make the different cities appear to slide through the viewport from one city to the next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/blues/accelerators-cellular-connected-electronic-kiosk/blob/main/web-app/styles.css" rel="noopener noreferrer"&gt;&lt;code&gt;styles.css&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;tonext&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;75&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;95&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;98&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;99&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;tostart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;75&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;95&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-300%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;98&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-300%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;99&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;snap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;96&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;97&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;99&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the code above, there are three separate animations defined.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tonext&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;tostart&lt;/code&gt;&lt;/strong&gt; - manage the horizontal position of each city's weather forecast at different stages of the carousel, creating a sliding effect either to the right or far left, followed by a reset to the starting position (for getting the animation to endlessly loop back to the first city in the list again).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;snap&lt;/code&gt;&lt;/strong&gt; - controls the &lt;code&gt;scroll-snap-align&lt;/code&gt; property, adjusting the snapping behavior at specific points in the animation timeline. This creates the dynamic snapping behavior that centers up the next city forecast in the carousel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By combining these three keyframes, we'll have the slick-looking animation in the demo video where the carousel appears to smoothly slide from one forecast to the next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Apply the keyframe animations to the appropriate HTML elements inside the &lt;code&gt;weather-forecast-container&lt;/code&gt; parent div.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now we're going to apply the &lt;code&gt;@keyframes&lt;/code&gt; animations defined in the previous step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/blues/accelerators-cellular-connected-electronic-kiosk/blob/main/web-app/styles.css" rel="noopener noreferrer"&gt;&lt;code&gt;styles.css&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.weather-carousel-snapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;animation-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tonext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;animation-timing-function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;animation-duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;animation-iteration-count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;infinite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.weather-template&lt;/span&gt;&lt;span class="nd"&gt;:last-child&lt;/span&gt; &lt;span class="nc"&gt;.weather-carousel-snapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;animation-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tostart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@media (hover: hover)&lt;/code&gt;&lt;/strong&gt; - in an effort to ensure that the carousel animations and scroll snap are not applied to devices where hovering isn't possible, such as with most smartphones and tablets, I wrapped these animations inside of the &lt;code&gt;hover&lt;/code&gt; media query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;.weather-carousel-snapper&lt;/code&gt; class is given the &lt;code&gt;tonext&lt;/code&gt; and &lt;code&gt;snap&lt;/code&gt; animations to run simultaneously with &lt;code&gt;animation-name: tonext, snap;&lt;/code&gt;, and it will ease over 4 seconds and loop infinitely.&lt;/p&gt;

&lt;p&gt;A rule targeting the last child of the .&lt;code&gt;weather-template&lt;/code&gt; element with the class of &lt;code&gt;weather-carousel-snapper&lt;/code&gt;, applies a different animation combination (&lt;code&gt;tostart&lt;/code&gt; and &lt;code&gt;snap&lt;/code&gt;), to reset the carousel after the last city's weather forecast has been displayed onscreen to get the carousel back to it's starting position (and first city in the list).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. And there you have it: a CSS only, auto-scrolling carousel with just HTML and CSS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The finished product demoed once more in this video.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In certain situations, it's really crucial that web applications be as small as possible. For instance, when that app may be running on a low power or underpowered system (like a Raspberry Pi).&lt;/p&gt;

&lt;p&gt;When I needed to build a small demo app that would download over cellular to a Raspi, I decided to do it with plain HTML, CSS, and a bit of vanilla-JS to keep the zip file size as small as possible - no JS frameworks like React or Svelte to help me out.&lt;/p&gt;

&lt;p&gt;I wanted to build a weather app that would display the forecast for a list of cities and auto-rotate through the list before returning to the beginning of the list to start over. At first, I thought I might need a carousel library to make this happen, but it turns out CSS scroll snap and keyframe animations can do exactly the same thing, no JS needed. That's pretty darn sweet.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you’d like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com" rel="noopener noreferrer"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope if you need a carousel in the future (auto rotating or not) that you'll try implementing it yourself with CSS - I think you'll be pleasantly surprised how little code is needed to make it happen. Enjoy!&lt;/p&gt;

&lt;h2&gt;
  
  
  References &amp;amp; Further Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CSS-Tricks, &lt;a href="https://css-tricks.com/css-only-carousel/" rel="noopener noreferrer"&gt;CSS-Only Carousel article&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN docs, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes" rel="noopener noreferrer"&gt;CSS @keyframes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN docs, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll_snap" rel="noopener noreferrer"&gt;CSS scroll snap module&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN docs, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type" rel="noopener noreferrer"&gt;CSS scroll-snap-type&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN docs, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-align" rel="noopener noreferrer"&gt;CSS scroll-snap-align&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub repo, &lt;a href="https://github.com/blues/accelerators-cellular-connected-electronic-kiosk" rel="noopener noreferrer"&gt;Cellular-Connected Electronic Kiosk&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>css</category>
    </item>
    <item>
      <title>Lean on CSS Clip Path to Make Cool Shapes in the DOM without Images</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Wed, 19 Jun 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/paigen11/lean-on-css-clip-path-to-make-cool-shapes-in-the-dom-without-images-2d7c</link>
      <guid>https://dev.to/paigen11/lean-on-css-clip-path-to-make-cool-shapes-in-the-dom-without-images-2d7c</guid>
      <description>&lt;p&gt;&lt;a href="/static/d5013df23a3e21be682ace94ae7684fb/5a190/shapes-hero.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2Fd5013df23a3e21be682ace94ae7684fb%2F1e043%2Fshapes-hero.png" title="Colorful shapes all slotted together" alt="Colorful shapes all slotted together" width="690" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Up until a few years ago if you wanted background shapes or sections of a website that were anything besides rectangles you most likely needed a designer to provide you with a static PNG or JPEG image that would be added as required, but CSS has come a long way since then, my friends.&lt;/p&gt;

&lt;p&gt;When I was working on a website update that broke up the contents on the page into different colored background sections, alternating between pure white and soft gray colors, the design mock up I had included one section whose bottom edge tilted up and to the right instead of going across the page at a perfect 90 degree angle, as a typical block element does.&lt;/p&gt;

&lt;p&gt;Now I could have asked the designer to make a background image to do this for me, but instead I wanted to see if I could do it on my own with the power of CSS. And lo and behold I could, with CSS &lt;code&gt;clip-path&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interesting shapes and visuals in the DOM are no longer purely the domain of designers, with tools like CSS &lt;code&gt;clip-path&lt;/code&gt;, devs have the power to reshape elements and I'll show you how.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  CSS clip-path
&lt;/h2&gt;

&lt;p&gt;If you're less familiar with the &lt;strong&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path" rel="noopener noreferrer"&gt;CSS &lt;code&gt;clip-path&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; property, like me, it creates a clipping region that sets which parts of an element should be shown. Parts that are inside the region are shown, while those outside are hidden.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/71fa44d62102e493c15a3c12ea452d07/442cb/clip-path-demo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F71fa44d62102e493c15a3c12ea452d07%2F1e043%2Fclip-path-demo.png" title="CSS clip-path demo" alt="CSS clip-path demo" width="690" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A demo from the MDN clip-path docs. Different clip-path options provide different views of the hot air balloon and text.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;clip-path&lt;/code&gt; property can accept a large variety of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path#syntax" rel="noopener noreferrer"&gt;values&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path#clip-source" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;clip-source&amp;gt;&lt;/code&gt;&lt;/a&gt;, which accepts values like &lt;code&gt;url&lt;/code&gt; for an SVG element with clipping path defined.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path#geometry-box" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;geometry-box&amp;gt;&lt;/code&gt;&lt;/a&gt;, which accepts values like &lt;code&gt;margin-box&lt;/code&gt; and &lt;code&gt;border-box&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/basic-shape" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;basic-shape&amp;gt;&lt;/code&gt;&lt;/a&gt;, which accepts values like &lt;code&gt;circle()&lt;/code&gt; and &lt;code&gt;rect()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;global-values&lt;/code&gt;, which accepts values like &lt;code&gt;inherit&lt;/code&gt; and &lt;code&gt;revert&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;geometry-box&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;basic-shape&amp;gt;&lt;/code&gt; values can even be combined together in one &lt;code&gt;clip-path&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* this CSS combines two different clip path properties */&lt;/span&gt;
&lt;span class="nt"&gt;clip-path&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;padding-box&lt;/span&gt; &lt;span class="nt"&gt;circle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;50&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;at&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt; &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;This post doesn't go into great detail about all of the properties &lt;code&gt;clip-path&lt;/code&gt; can accept and how they can be combined to create quite complex shapes. If you want more information and examples of &lt;code&gt;clip=path&lt;/code&gt; in action, I recommend starting with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path" rel="noopener noreferrer"&gt;Mozilla documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One of the &lt;code&gt;&amp;lt;basic-shape&amp;gt;&lt;/code&gt; properties &lt;code&gt;clip-path&lt;/code&gt; accepts is &lt;strong&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/basic-shape/polygon" rel="noopener noreferrer"&gt;&lt;code&gt;polygon()&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;, and this ended up being the solution I needed for my tilted background section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The polygon I needed to recreate with CSS
&lt;/h2&gt;

&lt;p&gt;&lt;a href="/static/9860a75b4f0ba90f2f0f82967b1d5025/6c68b/css-clip-path.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F9860a75b4f0ba90f2f0f82967b1d5025%2F1e043%2Fcss-clip-path.png" title="Website section with imperfect bottom angle courtesy of CSS clip path" alt="Website section with imperfect bottom angle courtesy of CSS clip path" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The gray polygon background I needed to create with CSS.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The image above is a screenshot of the gray background section I needed to recreate with CSS &lt;code&gt;clip-path&lt;/code&gt;'s &lt;code&gt;polygon()&lt;/code&gt; property. And the first thing I needed to do was create some HTML elements to apply the CSS to.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;polygon() clip-path vs rect() clip-path&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You might be wondering why I chose to use the &lt;code&gt;polygon()&lt;/code&gt; property instead of the &lt;code&gt;rect()&lt;/code&gt; property with &lt;code&gt;clip-path&lt;/code&gt;. While the two are similar, &lt;code&gt;polygon()&lt;/code&gt; can create more complex polygonal shapes and offers greater versatility for advanced designs by accepting pairs of coordinates to define each vertex of the polygon, whereas &lt;code&gt;rect()&lt;/code&gt; can only handle rectangular shapes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Set up the HTML and CSS
&lt;/h3&gt;

&lt;p&gt;The site I was working on relied on the static site generator &lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt;, a Go-based framework. Hugo uses &lt;a href="https://gohugo.io/templates/introduction/" rel="noopener noreferrer"&gt;templates&lt;/a&gt; to render the site's HTML, so the example code below should look relatively familiar to you if you know HTML.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on templates:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've ever used JSX components, Node.js with Pug or Handlebars, or Jekyll - Hugo's templates are similar: HTML elements with Go variables and functions sprinkled in with &lt;code&gt;{{ }}&lt;/code&gt; to render the correct information wherever the templates are injected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the code for what I'd nicknamed the "puzzle section" of the page due to the puzzle piece in the foreground of this section. For the purposes and clarity of this article, I've replaced the Go variables injected into the template with the generated HTML.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;single.html&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"about-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- more HTML elements up here --&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"puzzle-section section"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-12 col-md-6 col-lg-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;h4&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                Lorem ipsum dolor
              &lt;span class="nt"&gt;&amp;lt;/h4&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                Sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Ipsum dolor sit amet consectetur adipiscing elit pellentesque.
              &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;h4&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                Duis aute irure dolor in reprehenderit
              &lt;span class="nt"&gt;&amp;lt;/h4&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Consectetur adipiscing elit pellentesque habitant morbi tristique senectus et.
              &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
            &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-sm-8 offset-sm-2 col-md-6 offset-md-0 col-lg-6 offset-lg-0"&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
              &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"img-fluid"&lt;/span&gt;
              &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/images/about/puzzle-pieces.png"&lt;/span&gt;
              &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Puzzle pieces"&lt;/span&gt;
            &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

     &lt;span class="c"&gt;&amp;lt;!-- more HTML elements below --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This section of code is relatively compact, but it deserves discussion. In addition to the HTML elements, there are quite a few CSS classes which come from the &lt;strong&gt;&lt;a href="https://getbootstrap.com/" rel="noopener noreferrer"&gt;Bootstrap&lt;/a&gt;&lt;/strong&gt; library, one of the original open source CSS frameworks for responsive web designs.&lt;/p&gt;

&lt;p&gt;Among the custom classes like &lt;code&gt;about-body&lt;/code&gt;, which I used for adding custom styling, there are classes like &lt;code&gt;container&lt;/code&gt;, &lt;code&gt;row&lt;/code&gt;, &lt;code&gt;col-12&lt;/code&gt; or &lt;code&gt;col-md-6&lt;/code&gt;, &lt;code&gt;mb-5&lt;/code&gt;, and &lt;code&gt;mb-3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All of the latter classes are Bootstrap classes, which serve to make the text and image elements onscreen share the width of the page when the viewport is over a certain width (&lt;code&gt;col-md-6&lt;/code&gt;), or apply a &lt;code&gt;margin-bottom&lt;/code&gt; of a certain amount to the &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; tags (&lt;code&gt;mb-3&lt;/code&gt; or &lt;code&gt;mb-5&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The Bootstrap classes are beside the point for this post though, the class to focus on is the &lt;strong&gt;&lt;code&gt;puzzle-section&lt;/code&gt;&lt;/strong&gt; which wraps all the text and puzzle piece image.&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;puzzle-section&lt;/code&gt; class is where we're going to add the &lt;code&gt;clip-path&lt;/code&gt; property to display the light grey background behind the text and image with the slightly tilted, up-and-to-the-right design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add the CSS clip-path to shape puzzle-section
&lt;/h3&gt;

&lt;p&gt;As I wasn't quite sure how to style a normal, rectangular &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; into an uneven shape, I started looking for a solution online and found this helpful, interactive &lt;code&gt;clip-path&lt;/code&gt;-focused site, &lt;a href="https://bennettfeely.com/clippy/" rel="noopener noreferrer"&gt;CSS clip-path maker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/f804bb6617c0c319de6b241943cf8028/f98ee/clippy-website.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2Ff804bb6617c0c319de6b241943cf8028%2F1e043%2Fclippy-website.png" title="CSS clip-path maker website" alt="CSS clip-path maker website" width="690" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This CSS &lt;code&gt;clip-path&lt;/code&gt; maker website is fantastic because it has a whole slew of preset shapes, adjustable image sizes and backgrounds, and the currently displayed image's vertices can be dragged into any arrangement you want. The line at the bottom of the screen shows the exact &lt;code&gt;clip-path&lt;/code&gt; CSS values that you can copy/paste into your own project's CSS.&lt;/p&gt;

&lt;p&gt;I chose the parallelogram preset shape as my starting point, and then dragged the corners to match the angle of the background section I was trying to recreate from scratch. Once I was satisfied it looked accurate, I copied the CSS line at the bottom of the page to my clipboard.&lt;/p&gt;

&lt;p&gt;In my project's SCSS file, I added the copied &lt;code&gt;clip-path&lt;/code&gt; CSS in addition to the light grey &lt;code&gt;background-color&lt;/code&gt; property and some padding to give the text and puzzle piece images some breathing room on the page.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Even though this file shown in the example code is SCSS instead of pure CSS, for this post it shouldn't make a difference here. It should be a direct 1:1 comparison.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;about.scss&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.about-body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// this white sets the white background color for the whole webpage&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 

  &lt;span class="nc"&gt;.puzzle-section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// clip-path code copied from the clip-path maker website&lt;/span&gt;
    &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;polygon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;75%&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light-grey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;10rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That little bit of CSS for &lt;code&gt;clip-path&lt;/code&gt; was all that was needed to take my perfectly rectangular DOM element and turn it into an imperfect polygon instead. Not too shabby!&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;CSS is pushing the boundaries of what web developers can do without resorting to images, videos, and custom designed elements all the time. And the satisfaction of figuring out how to do a cool little bit of design on all on your own feels pretty empowering.&lt;/p&gt;

&lt;p&gt;A recent example of this was using the CSS &lt;code&gt;clip-path&lt;/code&gt; property to create a background box for some text and images that had an uneven bottom edge. With the help of an &lt;a href="https://bennettfeely.com/clippy/" rel="noopener noreferrer"&gt;interactive website dedicated decoding to clip-paths&lt;/a&gt; of all shapes and sizes, I was able to make quick work of this slightly skewed polygon.&lt;/p&gt;

&lt;p&gt;And let me take a moment to shout out how much I appreciate the folks putting out those little sites or code snippets that solve a very specific problem for another developer - you folks continue to make the Internet a better place.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you’d like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com" rel="noopener noreferrer"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope learning to reshape how elements look in the DOM with just the power of CSS helps you as much as it's helped me.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further References &amp;amp; Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MDN docs, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path" rel="noopener noreferrer"&gt;CSS clip-path&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bennettfeely.com/clippy/" rel="noopener noreferrer"&gt;CSS clip-path generator website&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>css</category>
    </item>
    <item>
      <title>Create a Custom Formatted CSV from Python Data</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Fri, 19 Apr 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/paigen11/create-a-custom-formatted-csv-from-python-data-28gd</link>
      <guid>https://dev.to/paigen11/create-a-custom-formatted-csv-from-python-data-28gd</guid>
      <description>&lt;p&gt;&lt;a href="/static/90aea02c105e7f30a970839d8be42bec/4b190/csv-data-hero.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F90aea02c105e7f30a970839d8be42bec%2F15ec7%2Fcsv-data-hero.jpg" title="Spreadsheet of numbers" alt="Spreadsheet of numbers" width="690" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In 2023, a friend of mine asked me to write a program to collect the data from an NFT collection on the &lt;a href="https://nftrade.com/" rel="noopener noreferrer"&gt;NFTrade&lt;/a&gt; website. He was interested in the following information: all the NFTs currently for sale in the collection, the current price of BNB (the cryptocurrency the NFTs are listed for sale in), the price of each NFT converted to US dollars based on the current price of BNB, and put all that data into a spreadsheet that's easy to manipulate, sort, and filter.&lt;/p&gt;

&lt;p&gt;I couldn't use my go-to JavaScript skills to fetch data via HTTP calls from the NFTrade site because there is no public-facing API, so instead, I taught myself to build a small web scraping script to visit the site and "scrape" that data from it.&lt;/p&gt;

&lt;p&gt;Python seems to be a very popular language for such projects, so I went with it, and as I worked on it, the requirements got a bit more complex, and I learned a lot of useful things about Python as a result, which I've shared in a series of blog posts. This post will be the last in my series covering this web scraper.&lt;/p&gt;

&lt;p&gt;In previous articles I:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.paigeniedringhaus.com/blog/scrape-data-from-a-lazy-loading-website-with-selenium-python" rel="noopener noreferrer"&gt;Scraped the NFT data off of NFTrade using the Selenium Python package&lt;/a&gt;,&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.paigeniedringhaus.com/blog/use-selenium-with-python-to-target-the-x-path-of-a-particular-object" rel="noopener noreferrer"&gt;Filtered each NFT's raw data into a separate object in a list of NFTs&lt;/a&gt;,&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.paigeniedringhaus.com/blog/filter-merge-and-update-python-lists-based-on-object-attributes" rel="noopener noreferrer"&gt;Narrowed the list down to NFTs just for sale, fetched the current price of BNB, and added the list price of each NFT in US dollars to each NFT&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And now, with all the data in one place, I needed to make it into a spreadsheet my friend could work with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Python csv.DictWriter module makes turning a list of Python objects into a standard CSV file a straightforward task, which I'll demonstrate in this article.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; I am not normally a Python developer so my code examples may not be the most efficient or elegant Python code ever written, but they get the job done.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Sample Python data
&lt;/h2&gt;

&lt;p&gt;I need to set the stage and show you the shape of the my Python data before I show you the solution I ended up using. As I mentioned in the introduction, the data I wanted for each NFT included the following things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NFT ID&lt;/li&gt;
&lt;li&gt;NFT list price (in BNB)&lt;/li&gt;
&lt;li&gt;NFT rarity score (a number randomly assigned to each NFT in the collection)&lt;/li&gt;
&lt;li&gt;Current price of BNB in US dollars&lt;/li&gt;
&lt;li&gt;Cost of NFT in US dollars&lt;/li&gt;
&lt;li&gt;Cost of each rarity point assigned to the NFT in US dollars&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I ended up with is a list of objects (or &lt;a href="https://www.w3schools.com/python/python_dictionaries.asp" rel="noopener noreferrer"&gt;"dictionaries"&lt;/a&gt; in Python parlance) because each object in the list is made up of key value pairs. Here's a sample of what the list of data looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample NFT data to be added to CSV file&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;352.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;28.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;281.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;352.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;77.54&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;387.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;352.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;98.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;174&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.4&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;493.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; 
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;352.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;29.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;184&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.6&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;563.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;352.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;46.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;704.88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;# more NFT data
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you're interested in seeing how I got this list of data, check out my &lt;a href="https://www.paigeniedringhaus.com/blog/filter-merge-and-update-python-lists-based-on-object-attributes" rel="noopener noreferrer"&gt;previous blog post&lt;/a&gt; for a more in-depth explanation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ok, now that I've established what the data looks like we can move on to how I turned this list into a CSV, complete with a header row of each key in the dictionary object.&lt;/p&gt;

&lt;h3&gt;
  
  
  The csv.DictWriter module
&lt;/h3&gt;

&lt;p&gt;Unlike core JavaScript, which tends to be pretty bare-bones and makes users install packages for most everything they want to do, the Python standard library is quite robust.&lt;/p&gt;

&lt;p&gt;It comes with a &lt;a href="https://docs.python.org/3/library/csv.html#" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;csv&lt;/code&gt; module&lt;/strong&gt;&lt;/a&gt; that implements classes to read and write tabular data in CSV format out of the box, as the CSV (Comma Separated Values) format is the most common import and export formats for spreadsheets and databases.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;csv&lt;/code&gt; module had two packages of particular interest to me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.python.org/3/library/csv.html#csv.writer" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;writer&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; - a writer object responsible for converting the user's data into delimited strings on a given file-like object.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.python.org/3/library/csv.html#csv.DictWriter" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;DictWriter&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; - an object like a writer but it maps dictionaries to output rows. This object requires a &lt;code&gt;fieldnames&lt;/code&gt; parameter that is a sequence of keys which identifies the order in which values in the dictionary are passed to the &lt;code&gt;writerow()&lt;/code&gt; method.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since my Python data is formatted as a list of dictionaries, as shown in the previous section, it made the most sense for me to use the &lt;code&gt;csv.DictWriter&lt;/code&gt; module.&lt;/p&gt;

&lt;p&gt;For me, the &lt;code&gt;fieldnames&lt;/code&gt; parameter would be composed of the keys of each dictionary: &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;bnb&lt;/code&gt;, &lt;code&gt;nft_price&lt;/code&gt;, etc. and that would guarantee that each dictionary of NFT info passed through the &lt;code&gt;DictWriter&lt;/code&gt; object would match up the proper value with its corresponding key. And &lt;code&gt;DictWriter&lt;/code&gt; also has two handy public methods I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.python.org/3/library/csv.html#csv.DictWriter.writeheader" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;writeheader()&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; - a method to write a row with the field names to the writer's file object (i.e. the top row in a CSV that typically has the column names listed for each column of data).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.python.org/3/library/csv.html#csv.csvwriter.writerows" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;writerows()&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; - the method that writes all the elements in rows to the writer's file object (i.e. the list of NFT objects I wanted added to the CSV file).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;DictWriter&lt;/code&gt; was the right choice for my situation, so let's get to how I put it into practice to make my file next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Format the Python data into a CSV and save it locally
&lt;/h3&gt;

&lt;p&gt;To transform the list of Python dictionaries into a CSV using the &lt;code&gt;csv.DictWriter&lt;/code&gt; module, I created a new function called &lt;code&gt;download_csv()&lt;/code&gt;, and here is what the code looked like in my &lt;code&gt;for_sale_scraper.py&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;download_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card_list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Turn card list into CSV file.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
      &lt;span class="n"&gt;date_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y_%m_%d-%I_%M_%p_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# add date time to the front of the file name 
&lt;/span&gt;      &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NFTs_For_Sale.csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;dict_writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
          &lt;span class="n"&gt;dict_writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="n"&gt;dict_writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nf"&gt;pprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CSV NFTs For Sale file generated!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will probably be easiest to go through this function line by line, so let's start from the top.&lt;/p&gt;

&lt;p&gt;The function takes in a &lt;code&gt;card_list&lt;/code&gt; argument which is what I want written to each row in the CSV, and the first thing it does is create a variable named &lt;code&gt;date_time&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;date_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y_%m_%d-%I_%M_%p_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;date_time&lt;/code&gt; is a formatted date and time string representing the current date in the format of &lt;code&gt;"Year_Month_Day-Hour_Minute_AM/PM"&lt;/code&gt;. When called, it will generate a string like &lt;code&gt;"2024_04_11-03_30_PM_"&lt;/code&gt;, which will be tacked on to the beginning of the CSV's file name so it's easy to sort and identify the files after they're generated.&lt;/p&gt;

&lt;p&gt;On to the next line of the function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NFTs_For_Sale.csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;with open&lt;/code&gt; opens a file for writing with the specified filename, which is the newly created &lt;code&gt;date_time&lt;/code&gt; variable concatenated with the &lt;code&gt;"NFTs_For_Sale.csv"&lt;/code&gt; string. The &lt;code&gt;'w'&lt;/code&gt; mode indicates the file will be opened for writing, the &lt;code&gt;encoding='utf8'&lt;/code&gt; parameter specifies the character encoding to be used, and the &lt;code&gt;newline=""&lt;/code&gt; ensures the proper newline character is used for writing rows to the CSV file. The whole thing is named &lt;code&gt;output_file&lt;/code&gt; for reference later in the function.&lt;/p&gt;

&lt;p&gt;Then we come to the &lt;code&gt;DictWriter&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;dict_writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;dict_writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;dict_writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is &lt;code&gt;DictWriter&lt;/code&gt;'s time to shine, as a new instance of it is created and the &lt;code&gt;output_file&lt;/code&gt; is passed to it, along with the &lt;code&gt;fieldnames&lt;/code&gt; list which includes all the dictionary keys as the headers for the CSV file. In the following two lines of code, &lt;code&gt;DictWriter&lt;/code&gt; writes the header row to the CSV file with the names specified in &lt;code&gt;fieldnames&lt;/code&gt;, and writes the list of dictionaries (the &lt;code&gt;card_list&lt;/code&gt; passed to this function) to the CSV file. Each dictionary in the list represents a row in the CSV file.&lt;/p&gt;

&lt;p&gt;Job complete!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;pprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CSV NFTs For Sale file generated!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, a success message is printed out at the end of the function indicating the CSV file has been successfully generated, and the file should show up in the repo alongside all the other previously project files.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/2d2b4be37e2e3bbc1a958a624a8f96a9/0a47e/csvs-in-repo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F2d2b4be37e2e3bbc1a958a624a8f96a9%2F0a47e%2Fcsvs-in-repo.png" title="CSVs being generated in local NFT scraper repo" alt="CSVs being generated in local NFT scraper repo" width="600" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note all the generated CSV files, neatly ordered in descending order by their filename dates.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And the &lt;code&gt;download_csv()&lt;/code&gt; function is called from the main Python function like in the code snippet below.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I've included the other functions called before &lt;code&gt;download_csv&lt;/code&gt; so you can see how I divvied up the work this file was doing, but I have not included the details of those functions because it's outside the scope of this post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;for_sale_scraper.py&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;scraper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ForSaleNFTScraper&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
   &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_card_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;card_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; 
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_nft_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# filter out any extra cards that aren't for sale
&lt;/span&gt;   &lt;span class="n"&gt;cards_for_sale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_priced_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# add rarity scores to all cards in the list by merging them with the id_rs_list
&lt;/span&gt;   &lt;span class="n"&gt;cards_with_rs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cards_rarity_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards_for_sale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

     &lt;span class="c1"&gt;# add the current bnb price, current usd price of cards and current usd price of each rs point
&lt;/span&gt;   &lt;span class="n"&gt;cards_with_bnb_rs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_pricing_to_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards_with_rs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# generate csv file 
&lt;/span&gt;   &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards_with_bnb_rs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, my friend can generate CSVs of the NFT collection whenever he wants to, or he could even set up a cron job on his machine to run this script on a regular basis and then review the generated files at his convenience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building a web scraper to collect data from the NFTrade site was a good exercise for me to learn more about Python, and it was a great opportunity for me to share my learnings in a series of blog posts for anyone else looking to do something similar.&lt;/p&gt;

&lt;p&gt;I scraped the data with Selenium Python, narrowed it down to the pieces I wanted, added some extra info like the cost of each NFT in US dollars, and then bundled all data up into a downloadable CSV for easy scanning and sorting.&lt;/p&gt;

&lt;p&gt;The extra nice thing about making it into a CSV is that the CSV reading and writing functions are part of the core Python library so I didn't even have to install any third party packages to make it work - it was actually quite straightforward in the end.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you’d like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com" rel="noopener noreferrer"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope learning to use Python's &lt;code&gt;csv.DictWriter&lt;/code&gt; module to make your own CSVs from list of data comes in handy in your own apps and projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  References &amp;amp; Further Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nftrade.com/" rel="noopener noreferrer"&gt;NFTrade&lt;/a&gt; website&lt;/li&gt;
&lt;li&gt;Python &lt;a href="https://docs.python.org/3/library/csv.html#csv.DictWriter" rel="noopener noreferrer"&gt;DictWriter documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;First blog post about &lt;a href="https://www.paigeniedringhaus.com/blog/scrape-data-from-a-lazy-loading-website-with-selenium-python" rel="noopener noreferrer"&gt;scraping data from a lazy-loading website using Selenium Python&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Second blog post about &lt;a href="https://www.paigeniedringhaus.com/blog/use-selenium-with-python-to-target-the-x-path-of-a-particular-object" rel="noopener noreferrer"&gt;limiting data searches to a particular element on a page instead of the whole page when using XPath&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Third blog post about &lt;a href="https://www.paigeniedringhaus.com/blog/filter-merge-and-update-python-lists-based-on-object-attributes" rel="noopener noreferrer"&gt;filtering, merging, and updating lists of objects in Python&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>bigdata</category>
      <category>csv</category>
    </item>
    <item>
      <title>Filter, Merge, and Update Python Lists Based on Object Attributes</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Fri, 23 Feb 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/paigen11/filter-merge-and-update-python-lists-based-on-object-attributes-4bm4</link>
      <guid>https://dev.to/paigen11/filter-merge-and-update-python-lists-based-on-object-attributes-4bm4</guid>
      <description>&lt;p&gt;&lt;a href="/static/b5215f82ecbd1930fbfdac6b8ca230fa/4b190/list-hero.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2Fb5215f82ecbd1930fbfdac6b8ca230fa%2F15ec7%2Flist-hero.jpg" title="Handwritten list" alt="Handwritten list" width="690" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Last year, I wrote a web scraping program to collect data from one of the NFT collections on the &lt;a href="https://nftrade.com/" rel="noopener noreferrer"&gt;NFTrade&lt;/a&gt; site. My friend wanted the following data included in a CSV: all the NFTs currently for sale in the collection, the total price of each NFT in US dollars based on the current market price of the BNB cryptocurrency that the NFT is for sale in, and the price in USD per rarity point (a value randomly assigned to each NFT in the collection).&lt;/p&gt;

&lt;p&gt;The NFTrade website does not have a public API so instead of writing a Node.js script to fetch the data via HTTP calls, I built a small site scraping script to go to the website and actually "scrape" the data from it.&lt;/p&gt;

&lt;p&gt;Having not written a web scraper before, I chose to write the program in Python, and as I built the scraper, the project requirements got a bit more complex, and I learned a bunch of useful techniques when coding in Python, which I'm sharing in a series of posts.&lt;/p&gt;

&lt;p&gt;After choosing the Selenium Python package to use Selenium WebDriver to scrape the data from NFTrade and extract the details from each NFT that I wanted (the NFT's ID and price in BNB), I needed to update my new list of NFT data in several ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I needed to filter out any NFTs that weren't currently for sale (some that were scraped off the site weren't actually for sale),&lt;/li&gt;
&lt;li&gt;I needed to match all the NFTs for sale with their "rarity scores" (as defined in a separate JSON list) and include those scores along with the rest of the NFT data,&lt;/li&gt;
&lt;li&gt;I needed to compute the total cost and cost per rarity point for each NFT in USD based on the current market price of BNB and add those prices to each NFT in the list as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I know this sounds quite complicated, but I broke each of these requirements down into separate methods inside my Python script and learned &lt;em&gt;a lot&lt;/em&gt; about working with lists in Python along the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today, I'll show you how to filter lists by whether an attribute exists in an object, how to merge two lists of items together based on matching attributes, and even how to add new object properties to the objects within a list in Python.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; I am not normally a Python developer so my code examples may not be the most efficient or elegant Python code ever written, but they get the job done.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Sample Python Data
&lt;/h2&gt;

&lt;p&gt;Before I dive into the specifics of my list manipulations in Python, let me give you a little background on what the data looks like that I was working with. Here's a small sample of what the list of NFT data looked like before I started mutating it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample NFT data scraped from the NFTrade site&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6774&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.22&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5710&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.16&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3187&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6482&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7689&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;335&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.057&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;597&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3936&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2834&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.649&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;763&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.65&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7683&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7914&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see from the output above, the original data I started with was pretty sparse: the ID number for each NFT and the price in BNB (if it existed) were the only pieces of data present in each object from the info scraped off the NFTrade site. I had my work cut out for me to clean this list up and add more useful data to it, so let's move on to how I did so in the next section.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you'd like to see more about how to scrape the browser data and gather just the necessary bits, read my first couple of blog posts &lt;a href="https://www.paigeniedringhaus.com/blog/scrape-data-from-a-lazy-loading-website-with-selenium-python" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://www.paigeniedringhaus.com/blog/use-selenium-with-python-to-target-the-x-path-of-a-particular-object" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Filter objects in a list on whether an attribute exists or not
&lt;/h3&gt;

&lt;p&gt;As I mentioned in the introduction, the first thing I needed to do was clean this list up by removing any NFTs that didn't have a price.&lt;/p&gt;

&lt;p&gt;Due to how I had to lazily load and scrape the data from the NFTrade website initially, there was a good chance there were a handful of NFTs I gathered up that weren't for sale, and therefore didn't have prices, so I needed to weed them out first.&lt;/p&gt;

&lt;p&gt;Technically every NFT in my list had an &lt;code&gt;nft_price&lt;/code&gt; attribute, but if there was no price listed in the card's scraped data, the &lt;code&gt;nft_price&lt;/code&gt; attribute was assigned &lt;code&gt;None&lt;/code&gt;, which proved very useful.&lt;/p&gt;

&lt;p&gt;Inside of the &lt;code&gt;__main__&lt;/code&gt; method in my Python script, I'd already scraped the data from the webpage with the &lt;code&gt;get_cards()&lt;/code&gt; method, then looped through the NFT data to grab just the bits of relevant data with the &lt;code&gt;get_nft_data()&lt;/code&gt; method. Now I wanted to filter down the cards to only include ones listed for sale.&lt;/p&gt;

&lt;p&gt;Here's the &lt;code&gt;__main__&lt;/code&gt; method code first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;for_sale_scraper.py&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;scraper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ForSaleNFTScraper&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
   &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_card_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;card_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; 
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_nft_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# filter out any extra cards that aren't for sale
&lt;/span&gt;   &lt;span class="n"&gt;cards_for_sale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_priced_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the method I came up with to filter down to just the NFTs for sale: &lt;code&gt;filter_priced_cards()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_priced_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card_list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Filter card list to only cards with NFT cost.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# filter out any cards in the list that don't have an NFT price equal to None
&lt;/span&gt;    &lt;span class="n"&gt;cards_for_sale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card_list&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cards_for_sale&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down what's happening in the second line of the &lt;code&gt;filter_priced_cards()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;I used Python's built-in &lt;a href="https://docs.python.org/3/library/functions.html#filter" rel="noopener noreferrer"&gt;&lt;code&gt;filter()&lt;/code&gt;&lt;/a&gt; function to iterate over the &lt;code&gt;card_list&lt;/code&gt; passed to the function to create a new list named &lt;code&gt;cards_for_sale&lt;/code&gt;. The &lt;a href="https://www.programiz.com/python-programming/anonymous-function" rel="noopener noreferrer"&gt;anonymous lambda function&lt;/a&gt; inside of &lt;code&gt;filter()&lt;/code&gt; takes each &lt;code&gt;card&lt;/code&gt; in the &lt;code&gt;card_list&lt;/code&gt; and returns &lt;code&gt;True&lt;/code&gt; if the &lt;code&gt;nft_price&lt;/code&gt; attribute of the card is not &lt;code&gt;None&lt;/code&gt;, and &lt;code&gt;False&lt;/code&gt; if it is - this is how it filters out all the cards that don't have a price.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;list()&lt;/code&gt; function that wraps the &lt;code&gt;filter()&lt;/code&gt; converts the result back to a list, because &lt;code&gt;filter()&lt;/code&gt; returns a filter object which is an iterator, not a list.&lt;/p&gt;

&lt;p&gt;And finally, the new &lt;code&gt;cards_for_sale&lt;/code&gt; list is returned.&lt;/p&gt;

&lt;h3&gt;
  
  
  [Merge two lists together by matching object keys
&lt;/h3&gt;

&lt;p&gt;Once the NFTs not for sale have been filtered out, the next step is to add the rarity score to each NFT based on its ID.&lt;/p&gt;

&lt;p&gt;For this particular set of NFTs, each NFT had a "rarity score" that had been randomly assigned to it. The rarity scores for each NFT were listed in a separate JSON file in the project and they look like this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;id_rs_score.json&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;more&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;rarity&lt;/span&gt; &lt;span class="nf"&gt;scores &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;below&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I needed to combine my list of &lt;code&gt;cards_for_sale&lt;/code&gt; with the rarity scores in the JSON file by matching up the &lt;code&gt;id&lt;/code&gt; attribute in each list of objects. For this task, I came up with the following function: &lt;code&gt;get_cards_rarity_score()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_cards_rarity_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card_list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Combine rarity scores with card list by ID.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# get rs data for each card from json file 
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id_rs_list.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;id_rs_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# merge together cards with id_rs_list by their matching ID numbers 
&lt;/span&gt;    &lt;span class="n"&gt;match_cards_with_rs_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_list&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;id_rs_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;itemgetter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nf"&gt;itemgetter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;combined_cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChainMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;match_cards_with_rs_list&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# filter out all the items in the merged list without a "for sale" value 
&lt;/span&gt;    &lt;span class="n"&gt;filtered_combined_cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;combined_cards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;filtered_combined_cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;filtered_combined_cards&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To combine the rarity score with any of the NFT objects contained in the &lt;code&gt;card_list&lt;/code&gt; list, the first thing that had to happen was to read the data from the &lt;code&gt;id_rs_list.json&lt;/code&gt; file and assign it to a variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# get rs data for each card from json file 
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id_rs_list.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;id_rs_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the JSON list was extracted from the file, the &lt;code&gt;card_list&lt;/code&gt; and &lt;code&gt;id_rs_list&lt;/code&gt; needed to be merged together based on their matching IDs.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.python.org/3/library/itertools.html#itertools.groupby" rel="noopener noreferrer"&gt;&lt;code&gt;groupby()&lt;/code&gt;&lt;/a&gt; function groups elements with the same ID, and then &lt;a href="https://docs.python.org/3/library/collections.html#collections.ChainMap" rel="noopener noreferrer"&gt;&lt;code&gt;ChainMap()&lt;/code&gt;&lt;/a&gt; merged the grouped items into Python &lt;a href="https://www.w3schools.com/python/python_dictionaries.asp" rel="noopener noreferrer"&gt;dictionaries&lt;/a&gt; (objects). The result was a list of dictionaries (&lt;code&gt;combined_cards&lt;/code&gt;) where each dictionary represented a card with combined information from both lists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# merge together cards with id_rs_list by their matching ID numbers 
&lt;/span&gt;    &lt;span class="n"&gt;match_cards_with_rs_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_list&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;id_rs_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;itemgetter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nf"&gt;itemgetter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;combined_cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChainMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;match_cards_with_rs_list&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing to note: the &lt;code&gt;combined_cards&lt;/code&gt; list has &lt;em&gt;every&lt;/em&gt; NFT listed from the &lt;code&gt;id_rs_list&lt;/code&gt;, not just the ones whose IDs match the IDs in the &lt;code&gt;card_list&lt;/code&gt;. So the &lt;code&gt;combined_cards&lt;/code&gt; list looks like the data below - but for every item in &lt;code&gt;id_rs_list&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt; 
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.5&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="c1"&gt;# more IDs and NFT data 
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the &lt;code&gt;combined_cards&lt;/code&gt; list had &lt;em&gt;every single NFT&lt;/em&gt; in it (not just ones for sale), once more I had to filter the list down so that every item without an &lt;code&gt;"nft_price"&lt;/code&gt; was omitted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# filter out all the items in the merged list without a "for sale" value 
&lt;/span&gt;    &lt;span class="n"&gt;filtered_combined_cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;combined_cards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;filtered_combined_cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, since there's a (very likely) chance the NFT data in the &lt;code&gt;combined_cards&lt;/code&gt; list did not have the &lt;code&gt;"nft_price"&lt;/code&gt; attribute, I checked if each card had the key &lt;code&gt;"nft_price"&lt;/code&gt; and if so, the card was added to the new &lt;code&gt;filtered_combined_cards&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;filtered_combined_cards&lt;/code&gt; list ended up looking like the code snippet below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;174&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.4&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;184&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.6&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;335&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;562&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;584&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;597&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="c1"&gt;# more NFT data here
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once all this data manipulation and list combining was done, the function returned the final list of cards (&lt;code&gt;filtered_combined_cards&lt;/code&gt;) that had both rarity score information and an &lt;code&gt;"nft_price"&lt;/code&gt; attribute included.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;filtered_combined_cards&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For reference, here's the &lt;code&gt;__main__&lt;/code&gt; function in the Python script, which called the &lt;code&gt;get_cards_rarity_score()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;for_sale_scraper.py&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;scraper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ForSaleNFTScraper&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
   &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_card_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;card_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; 
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_nft_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# filter out any extra cards that aren't for sale
&lt;/span&gt;   &lt;span class="n"&gt;cards_for_sale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_priced_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# filter out any extra cards that aren't for sale
&lt;/span&gt;   &lt;span class="n"&gt;cards_for_sale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_priced_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add new object properties to each object in a list
&lt;/h3&gt;

&lt;p&gt;All right, here's the last Python list manipulation tip I'll be sharing in this post: how to add new properties to each object in a list.&lt;/p&gt;

&lt;p&gt;After filtering the NFTs to just the ones for sale, and adding the rarity scores from the &lt;code&gt;id_rs_list&lt;/code&gt; JSON file, I needed to fetch the current price of 1 BNB compared to US dollars, calculate the current price of each NFT in USD, and calculate the cost per rarity point for each NFT.&lt;/p&gt;

&lt;p&gt;Fortunately the cryptocurrency data aggregation site &lt;a href="https://www.coingecko.com/" rel="noopener noreferrer"&gt;CoinGecko&lt;/a&gt;, has a REST API that I could use to get the current market price of BNB cryptocurrency in US dollars, and then calculate the rest of the required data based on the info in my NFT card list.&lt;/p&gt;

&lt;p&gt;Here is the &lt;code&gt;add_pricing_to_cards()&lt;/code&gt; function I came up with to calculate the prices.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_pricing_to_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card_list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get current price of BNB and compute cost per rarity point&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.coingecko.com/api/v3/simple/price?ids=binancecoin&amp;amp;vs_currencies=USD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;bnb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;binancecoin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# add the current value of bnb to the card_list 
&lt;/span&gt;    &lt;span class="n"&gt;cards_bnb_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bnb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bnb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;card_list&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# compute the current price of usd for each card based on its bnb price       
&lt;/span&gt;    &lt;span class="n"&gt;cards_with_usd_price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards_bnb_price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# compute the current cost usd of each rarity score point         
&lt;/span&gt;    &lt;span class="n"&gt;cards_with_rs_prices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_per_rs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards_with_usd_price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cards_with_rs_prices&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the function, the first thing I did was call the CoinGecko price API to get the current price of BNB in USD.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.coingecko.com/api/v3/simple/price?ids=binancecoin&amp;amp;vs_currencies=USD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;bnb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;binancecoin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I added the &lt;code&gt;bnb&lt;/code&gt; to each object in the input &lt;code&gt;card_list&lt;/code&gt; and created a new list named &lt;code&gt;cards_bnb_price&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# add the current value of bnb to the card_list 
&lt;/span&gt;    &lt;span class="n"&gt;cards_bnb_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bnb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bnb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;card_list&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After including the current BNB price in USD, I was able to compute the total price in USD for each NFT in the list by multiplying the card's original price in BNB by the current price of BNB in USD.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# compute the current price of usd for each card based on its bnb price       
&lt;/span&gt;    &lt;span class="n"&gt;cards_with_usd_price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards_bnb_price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And I also calculated the price in USD per rarity score point as well, simply by dividing the card's total price in USD by the rarity score number (&lt;code&gt;rs&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# compute the current cost usd of each rarity score point         
&lt;/span&gt;    &lt;span class="n"&gt;cards_with_rs_prices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_per_rs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards_with_usd_price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function then returned the list of cards with the added pricing info, including BNB price, USD price, and USD cost per rarity score point. The final list data looked like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;352.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;28.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;281.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;352.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;77.54&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;387.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;352.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;98.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;174&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.4&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;493.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; 
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;352.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;29.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;184&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.6&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;563.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bnb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;352.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cost_per_rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;46.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;704.88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;# more NFT data
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;add_pricing_to_cards()&lt;/code&gt; function is called from the main Python function like so:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;for_sale_scraper.py&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;scraper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ForSaleNFTScraper&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
   &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_card_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;card_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; 
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_nft_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# filter out any extra cards that aren't for sale
&lt;/span&gt;   &lt;span class="n"&gt;cards_for_sale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_priced_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# filter out any extra cards that aren't for sale
&lt;/span&gt;   &lt;span class="n"&gt;cards_for_sale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_priced_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# add rarity scores to all cards in the list by merging them with the id_rs_list
&lt;/span&gt;   &lt;span class="n"&gt;cards_with_rs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cards_rarity_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards_for_sale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now that I had all the data that my friend requested for each NFT in the collection for sale on NFTrade, all that was left to do was turn the whole list into a downloadable CSV that would be easy to sort and manipulate. I'll save that for a future post.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When I had to use Python to build a website scraper to get NFT data off of the NFTrade site, I learned a lot of useful new coding tricks along the way.&lt;/p&gt;

&lt;p&gt;After I'd managed to scrape the data with the help of Selenium Python, and extract the initial data I needed from each NFT by using WebDriver's XPath, my job was far from complete.&lt;/p&gt;

&lt;p&gt;I needed to take the little data I had and combine those NFTs with "rarity scores" in a JSON file, fetch the current market price for BNB cryptocurrency in US dollars, and then compute the total cost of each NFT and cost per rarity point, and as I completed these tasks I learned a heck of a lot about how to work with lists of complex objects in various new ways. And I feel confident these new techniques will help me out in any future Python endeavors I might undertake.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more blogs about the problems I had to solve while building this Python website scraper in addition to other topics on JavaScript or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you’d like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com" rel="noopener noreferrer"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope learning to filter, merge, and alter objects within lists in Python proves helpful for you in your own projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further References &amp;amp; Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nftrade.com/" rel="noopener noreferrer"&gt;NFTrade&lt;/a&gt; website&lt;/li&gt;
&lt;li&gt;Python &lt;a href="https://docs.python.org/3/library/index.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.coingecko.com/" rel="noopener noreferrer"&gt;CoinGecko&lt;/a&gt; cryptocurrency tracker and analytics site&lt;/li&gt;
&lt;li&gt;First blog post about &lt;a href="https://www.paigeniedringhaus.com/blog/scrape-data-from-a-lazy-loading-website-with-selenium-python" rel="noopener noreferrer"&gt;scraping data from a lazy-loading website using Selenium Python&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Follow up blog post about &lt;a href="https://www.paigeniedringhaus.com/blog/use-selenium-with-python-to-target-the-x-path-of-a-particular-object" rel="noopener noreferrer"&gt;limiting data searches to a particular element on a page instead of the whole page when using XPath&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
    </item>
    <item>
      <title>Use Selenium with Python to Target the XPath of a Particular Object</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/paigen11/use-selenium-with-python-to-target-the-xpath-of-a-particular-object-n3p</link>
      <guid>https://dev.to/paigen11/use-selenium-with-python-to-target-the-xpath-of-a-particular-object-n3p</guid>
      <description>&lt;p&gt;&lt;a href="///static/ff3ffa49ef2dcc688c0e993fbcde1f69/0f98f/dashboard-hero.jpg"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bctb_4J9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.paigeniedringhaus.com/static/ff3ffa49ef2dcc688c0e993fbcde1f69/15ec7/dashboard-hero.jpg" alt="Webpage analytics dashboard" title="Webpage analytics dashboard" width="690" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Earlier this year, I needed to write a program for my friend to collect the data of one of the NFT collections on the &lt;a href="https://nftrade.com/"&gt;NFTrade&lt;/a&gt; site. He wanted the following data included: all the NFTs currently for sale in the collection and the price of each NFT in US dollars based on the current market price of the BNB cryptocurrency that the NFT is for sale in, and he wanted it listed out line by line in a CSV file that he could sort and manipulate.&lt;/p&gt;

&lt;p&gt;Unfortunately, the NFTrade website does not have a public API so instead of writing a Node.js script to fetch the data via HTTP calls, I built a small script to navigate to the website page and actually "scrape" the data off of it.&lt;/p&gt;

&lt;p&gt;Having not written a web scraper before, I chose to write the program in Python as it seems to be a very popular programming language choice for a task such as this. While I built this scraper, the project requirements evolved and got more complex, and I learned a bunch of useful new techniques when using Python, which I'll be sharing in a series of posts over the coming months.&lt;/p&gt;

&lt;p&gt;After I'd settled on using the Selenium Python package to start a Selenium WebDriver instance and scrape the data from NFTrade, I hit a snag: I was collecting all the individual NFT data to loop through and pull the details from, but each time the loop ran it only collected the data from the first NFT in the list.&lt;/p&gt;

&lt;p&gt;I was stumped and turned to the Internet for help and (once again) Stack Overflow came through for me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When extracting scraped web data using Selenium WebDriver's XPath to target the data, in order to search inside a particular element instead of the whole document, a period (&lt;code&gt;.&lt;/code&gt;) must be added in front of the XPath. I'll show you how to do it in this post.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; I am not normally a Python developer so my code examples may not be the most efficient or elegant Python code ever written, but they get the job done.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Selenium Python package
&lt;/h2&gt;

&lt;p&gt;For my project, I ended up using the &lt;a href="https://selenium-python.readthedocs.io/"&gt;&lt;strong&gt;Selenium Python package&lt;/strong&gt;&lt;/a&gt; because it can scrape websites with dynamically loaded data, which is how NFTrade works. A user lands on a collection of NFTs and as they scroll down the page, periodically more NFTs are loaded into the browser window via JavaScript.&lt;/p&gt;

&lt;p&gt;Selenium Python operates the Selenium WebDriver software and drives a browser just like a user would, and although its original use was for automated end-to-end testing of software, it can also be used to scrape data off of live web pages.&lt;/p&gt;

&lt;p&gt;If you'd like to read more about how to use Selenium Python for this, I encourage you to visit &lt;a href="https://www.paigeniedringhaus.com/blog/scrape-data-from-a-lazy-loading-website-with-selenium-python"&gt;my first blog post&lt;/a&gt; on this subject where I go in depth on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is XPath?
&lt;/h3&gt;

&lt;p&gt;Among the many useful methods included with the Selenium Python package are the methods &lt;a href="https://selenium-python.readthedocs.io/api.html#selenium.webdriver.remote.webdriver.WebDriver.find_element"&gt;&lt;code&gt;find_element()&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://selenium-python.readthedocs.io/api.html#selenium.webdriver.remote.webdriver.WebDriver.find_elements"&gt;&lt;code&gt;find_elements()&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://selenium-python.readthedocs.io/api.html#locate-elements-by"&gt;&lt;code&gt;By.XPATH&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;find_element&lt;/code&gt; methods do what their name implies: find an element (or elements) given a &lt;code&gt;By&lt;/code&gt; strategy and locator.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://selenium-python.readthedocs.io/api.html#locate-elements-by"&gt;&lt;code&gt;By&lt;/code&gt;&lt;/a&gt; accepts element IDs, names, attributes, XPaths, class names, etc. And &lt;a href="https://www.w3schools.com/xml/xml_xpath.asp"&gt;XPath&lt;/a&gt; is a syntax used to navigate through elements and attributes in a standard XML document (webpage).&lt;/p&gt;

&lt;p&gt;Due to the NFTrade site's structure, I used XPath expressions to identify all the individual NFTs on the page and scrape the data from each NFT to include later in my CSV file.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: XPath was targeting the whole document
&lt;/h3&gt;

&lt;p&gt;After I'd written the code to initially fire up my Selenium WebDriver instance, navigate to the NFTrade site, and load the NFTs into the browser that I wanted to scrape the data from, I had a list of NFT info I needed to slim down to just the data points I wanted to include in the CSV.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you'd like to see how to scrape the browser data in-depth, read my first blog post &lt;a href="https://www.paigeniedringhaus.com/blog/scrape-data-from-a-lazy-loading-website-with-selenium-python"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For me, this was things like: the NFT ID number (part of the NFT card's name) and the the NFT's sale price in BNB.&lt;/p&gt;

&lt;p&gt;Inside of the &lt;code&gt;__main__&lt;/code&gt; method in my Python script, I'd scraped the data from the webpage with the &lt;code&gt;get_cards()&lt;/code&gt; method, and then I wanted to loop through the NFT data I'd collected and extract the data points from each NFT card with my &lt;code&gt;get_nft_data()&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Here's the &lt;code&gt;__main__&lt;/code&gt; method code for reference:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;for_sale_scraper.py&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;scraper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ForSaleNFTScraper&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
   &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_card_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;card_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_nft_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; 
    &lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# pprint the card data to ensure we're getting the correct data out of each card
&lt;/span&gt;    &lt;span class="nf"&gt;pprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the code for my &lt;code&gt;get_nft_data()&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_nft_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nft_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Extracts and prints out NFT specific data.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;nft_name_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XPATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;//div[contains(@class, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item_itemName__ckoHR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;nft_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nft_name_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;innerHTML&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;nft_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nft_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;nft_price_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nft_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XPATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;//div[contains(@class, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item_itemPriceValueTxt__lblqJ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;nft_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nft_price_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;innerHTML&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nft_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nft_price&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What my &lt;code&gt;get_nft_data()&lt;/code&gt; method is &lt;em&gt;supposed&lt;/em&gt; to do is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use the XPath within each card to get the NFT's name via &lt;a href="https://selenium-python.readthedocs.io/api.html#selenium.webdriver.remote.webelement.WebElement.get_attribute"&gt;&lt;code&gt;get_attribute("innerHTML")&lt;/code&gt;&lt;/a&gt; (&lt;code&gt;innerHTML&lt;/code&gt; targets the text content of the element which includes its ID number at the end of the name), &lt;a href="https://www.w3schools.com/python/ref_string_partition.asp"&gt;&lt;code&gt;partition()&lt;/code&gt;&lt;/a&gt; the string into a tuple based on the &lt;code&gt;#&lt;/code&gt; included in the name, and take the last element in the tuple (which is the ID number).&lt;/li&gt;
&lt;li&gt;It should also get the NFT's price (also using &lt;code&gt;get_attribute("innerHTML")&lt;/code&gt;) via a second XPath within the NFT card.&lt;/li&gt;
&lt;li&gt;Finally return those two elements together as a new object with the keys of &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;price&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In theory, I wanted that to happen for every NFT I'd collected into my &lt;code&gt;card_data&lt;/code&gt; list. In practice, I got 200 elements that all contained the ID and price data from the very first card in my &lt;code&gt;card_data&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;Not quite what I was expecting.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: How to restrict XPath inside a particular element
&lt;/h3&gt;

&lt;p&gt;After several failed variations of the code above and searching through many Stack Overflow posts without success, I finally wrote my own SO post explaining my situation and asking for help from the greater web development community.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you'd like to see my original Stack Overflow post and the helpful responses provided, &lt;a href="https://stackoverflow.com/questions/75604911/python-web-scraping-with-selenium-only-extracts-first-elements-data-in-list-of"&gt;here&lt;/a&gt; is a link.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Just over 30 minutes after posting my question, a kind soul answered it and got me moving forward again. That's the power of the web dev community at its finest.&lt;/p&gt;

&lt;p&gt;Below is the corrected &lt;code&gt;get_nft_data()&lt;/code&gt; code that actually gets the data from each individual NFT as the data is looped over. I also added some comments between lines of code to explain what's happening at each step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_nft_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nft_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Extracts and prints out card specific data.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
      &lt;span class="c1"&gt;# get full card name "NFT_CARD #1234" by XPATH
&lt;/span&gt;      &lt;span class="n"&gt;nft_name_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nft_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XPATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.//div[contains(@class, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item_itemName__ckoHR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;nft_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nft_name_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
      &lt;span class="c1"&gt;# parse out just ID number from name
&lt;/span&gt;      &lt;span class="n"&gt;nft_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nft_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

      &lt;span class="c1"&gt;# get nft recently sold value by XPATH
&lt;/span&gt;      &lt;span class="n"&gt;nft_bnb_sale_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nft_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XPATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.//div[contains(@class, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item_itemPriceValueTxt__lblqJ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="c1"&gt;# if there is a for sale price, take it 
&lt;/span&gt;      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nft_bnb_sale_price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;nft_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nft_bnb_sale_price&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
      &lt;span class="c1"&gt;# if there's no value, just put None in place of a value    
&lt;/span&gt;          &lt;span class="n"&gt;nft_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;   

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nft_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
          &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nft_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nft_price&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this version of the Python code, there's three main differences.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The first is that instead of using &lt;code&gt;self.driver.find_element&lt;/code&gt;, this code is using &lt;code&gt;nft_data.find_element&lt;/code&gt;. By substituting &lt;code&gt;nft_data&lt;/code&gt; instead of &lt;code&gt;self.driver&lt;/code&gt;, it allows the XPath search to be restricted to a particular element.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Second, inside of each &lt;code&gt;find_element&lt;/code&gt; method referencing &lt;code&gt;By.XPATH&lt;/code&gt;, the XPath being passed has a &lt;code&gt;.&lt;/code&gt; in front of it. So &lt;code&gt;'//div[contains(@class, "Item_itemName_ckoHR")]'&lt;/code&gt; becomes &lt;code&gt;'.//div[contains(@class, "Item_itemName__ckoHR")]'&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Last, the user suggested I could use &lt;code&gt;.text&lt;/code&gt; to get the NFT name and BNB price instead of having to write out the lengthier &lt;code&gt;.get_attribute("innerHTML")&lt;/code&gt; each time to reach the text in the NFT, which was a nice improvement in code readability.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The solution also mentioned there was a chance that some of the NFTs collected from the page may not have a price listed (NFTrade displays all NFTs in a collection, not just the ones for sale), and recommended wrapping the code that gets the &lt;code&gt;nft_price&lt;/code&gt; in an &lt;code&gt;if / else&lt;/code&gt; block so if the price is present, it will be collected and returned, and if it's not, it'll return &lt;code&gt;None&lt;/code&gt; in place of the value and not throw an error in the code.&lt;/p&gt;

&lt;p&gt;Hence this code for checking sale price:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt; &lt;span class="c1"&gt;# if there is a for sale price, take it 
&lt;/span&gt;      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nft_bnb_sale_price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;nft_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nft_bnb_sale_price&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
      &lt;span class="c1"&gt;# if there's no value, just put None in place of a value    
&lt;/span&gt;          &lt;span class="n"&gt;nft_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test the refactored code
&lt;/h3&gt;

&lt;p&gt;With my newly refactored &lt;code&gt;get_nft_data()&lt;/code&gt; method at the ready, it was time to test it out in my Python script.&lt;/p&gt;

&lt;p&gt;As a reminder, my &lt;code&gt;__main__&lt;/code&gt; method looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;scraper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ForSaleNFTScraper&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
   &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_card_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;card_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; 
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_nft_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# pprint the card data to ensure we're getting the correct data out of each card
&lt;/span&gt;    &lt;span class="nf"&gt;pprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, when I ran the script from the command line with &lt;code&gt;python for_sale_scraper.py&lt;/code&gt;, here's a screenshot of the output I received.&lt;/p&gt;

&lt;p&gt;&lt;a href="///static/24031d6508cf9ab4329fdbcc01f27135/477c9/nft-xpath-data-output.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mOlwdDX3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.paigeniedringhaus.com/static/24031d6508cf9ab4329fdbcc01f27135/477c9/nft-xpath-data-output.png" alt="Output from each of the NFT cards run through the get_nft_data() method" title="Output from each of the NFT cards run through the get\_nft\_data() method" width="288" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you look closely you'll see that each object in this area has a different ID and price in it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As you can see from the image, I got an array of items and each item in the array had a different &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;nft_price&lt;/code&gt; from the others. Now the &lt;code&gt;get_nft_data()&lt;/code&gt; method was working correctly, targeting the next NFT in the &lt;code&gt;card_data&lt;/code&gt; array with each successive iteration in the loop and pulling out the data specific to that card.&lt;/p&gt;

&lt;p&gt;Success!&lt;/p&gt;

&lt;p&gt;With that hurdle overcome, I was ready to move on to the next steps of this project: converting the NFT's BNB prices into the current USD prices and assembling them into a CSV spreadsheet. Those tasks will be covered in detail in upcoming blog posts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When I was asked to put together a spreadsheet of all the NFTs for sale in a particular collection on NFTrade, I ended up using Python to build a website scraper to accomplish it and learned a lot of new problem-solving techniques in the process.&lt;/p&gt;

&lt;p&gt;I managed to load and collect all the NFT data from a web page with the assistance of the Selenium Python package, but I got throughly stuck when trying to iterate through my data to extract the ID and price for each NFT from it.&lt;/p&gt;

&lt;p&gt;Luckily, the Stack Overflow community came through for me and taught me about the finer points of using Selenium WebDriver's XPath to target specific elements within a page instead of the whole page when looking for particular pieces of data, which got me unstuck and on my way to building my spreadsheet of data.&lt;/p&gt;

&lt;p&gt;Thank goodness for the knowledge sharing of the online web development community - I am truly grateful to be able to turn to them when I've exhausted my own ideas to solve a problem.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more blogs about the problems I had to solve while building this Python website scraper in addition to other topics on JavaScript or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you’d like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope learning how to restrict XPath searches to a particular element on a page instead of the entire page proves helpful for you in the future like was for me.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further References &amp;amp; Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://selenium-python.readthedocs.io/"&gt;Selenium Python&lt;/a&gt; docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.selenium.dev/documentation/webdriver/"&gt;Selenium WebDriver&lt;/a&gt; docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nftrade.com/"&gt;NFTrade&lt;/a&gt; website&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.w3schools.com/xml/xml_xpath.asp"&gt;XPath&lt;/a&gt; documentation&lt;/li&gt;
&lt;li&gt;Original &lt;a href="https://stackoverflow.com/questions/75604911/python-web-scraping-with-selenium-only-extracts-first-elements-data-in-list-of"&gt;Stack Overflow post&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Previous blog post about &lt;a href="https://www.paigeniedringhaus.com/blog/scrape-data-from-a-lazy-loading-website-with-selenium-python"&gt;scraping data from a lazy-loading website using Selenium Python&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>selenium</category>
      <category>bigdata</category>
      <category>webdriver</category>
    </item>
    <item>
      <title>Scrape Data from a Lazy Loading Website with Selenium Python</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Sun, 22 Oct 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/paigen11/scrape-data-from-a-lazy-loading-website-with-selenium-python-5h7j</link>
      <guid>https://dev.to/paigen11/scrape-data-from-a-lazy-loading-website-with-selenium-python-5h7j</guid>
      <description>&lt;p&gt;&lt;a href="/static/7a77cf89b7488e0364a6eb1c9e9e3a31/c1b63/python-selenium-nftrade-hero.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F7a77cf89b7488e0364a6eb1c9e9e3a31%2F1e043%2Fpython-selenium-nftrade-hero.png" title="Python logo, Selenium Python logo, NFTrade site" alt="Python logo, Selenium Python logo, NFTrade site" width="690" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A few months ago, my friend wanted me to write a program to collect the data of one of the NFT collections on the &lt;a href="https://nftrade.com/" rel="noopener noreferrer"&gt;NFTrade&lt;/a&gt; site, compute the current price of each NFT in US dollars based on the current market price of the BNB cryptocurrency it was listed for sale in, and compile all of the NFTs for sale into a CSV file that he could sort and manipulate.&lt;/p&gt;

&lt;p&gt;Unfortunately, the NFTrade website does not have a public API so writing a Node.js script to fetch the data from the API and format it as required was not an option. Instead, I needed to make a site scraper to actually go to the website page and "scrape" the data off of it.&lt;/p&gt;

&lt;p&gt;Having not written a web scraper before (and also wanting to make the script easier for my friend to update and run on his own machine), I decided to write the program in Python (it seems to be a very popular programming language choice for a task such as this). Along the way, my little web scraper's requirements evolved and got more complex, and I learned a bunch of useful new techniques about using Python for my project, which I intend to share in a series of posts over the coming months.&lt;/p&gt;

&lt;p&gt;My first attempt to scrape the data from NFTrade was unsuccessful beyond locating the first 75 NFTs on the page. I figured out this was because NFTrade (as many other websites do) lazy loads NFTs onto the page 75 at a time: once the user's scrolled down far enough to reach the end of the currently visible items, the site loads the next batch of elements onto the page (essentially a fancier version of pagination). So I needed a way to have my web scraper program collect whatever data was available on the page then scroll down far enough to trigger more data to load and collect that, and rinse and repeat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After some trial and error, I finally found a working solution with the help of a Python package named Selenium Python, and I'll share with you today how to write your own Python script to scrape data from a lazy loading website with Selenium WebDriver.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; I am not normally a Python developer so my code examples may not be the most efficient or elegant Python code ever written, but they get the job done.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Selenium with Python package
&lt;/h2&gt;

&lt;p&gt;There are a few different popular Python packages available for web scraping which I tried before reaching for Selenium, but I had an issue with them in that they only worked for static websites that were generated at build time, not for sites that are generated on the client-side via JavaScript, like NFTrade is.&lt;/p&gt;

&lt;p&gt;To that end, I had to do a little digging to find a package that could work with scraping sites with dynamically loaded data, and I ran across the &lt;a href="https://selenium-python.readthedocs.io/" rel="noopener noreferrer"&gt;&lt;strong&gt;Selenium Python&lt;/strong&gt;&lt;/a&gt; package during my investigation.&lt;/p&gt;

&lt;p&gt;Selenium Python is a Python-based API that allows users to write scripts or automated tests using &lt;a href="https://www.selenium.dev/documentation/webdriver/" rel="noopener noreferrer"&gt;Selenium WebDriver&lt;/a&gt; in an intuitive, Python-flavored way. And Selenium WebDriver is a software that can drive a browser natively, as a user would, either locally or on a remote machine. Originally created back in 2004, some version of Selenium has been around for years and is considered one of the earliest versions of automated testing that emulates user actions on a web page (commonly known today as end-to-end testing).&lt;/p&gt;

&lt;p&gt;The cool thing about WebDriver though, is that its uses span beyond automation testing, as scripts can actually be written to scrape data off of live web pages, and that's just what I ended up doing in my Python script, so let's get started.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Selenium Python in the Python project
&lt;/h3&gt;

&lt;p&gt;As with most projects, the first thing to do is add the Selenium Python package to the Python project. The easiest way is to use &lt;a href="https://pip.pypa.io/en/latest/installation/" rel="noopener noreferrer"&gt;pip&lt;/a&gt; to install the Selenium package.&lt;/p&gt;

&lt;p&gt;Assuming you have pip on your machine, at the root of your Python project folder, run the following command from a terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;selenium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, add the &lt;code&gt;selenium&lt;/code&gt; package to your &lt;code&gt;requirements.txt&lt;/code&gt; file so anyone downloading the repo in the future can install all the necessary project dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;requirements.txt&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selenium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's all it takes to be ready to use WebDriver in your Python script. Simple enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Import Selenium WebDriver into Python script
&lt;/h3&gt;

&lt;p&gt;After adding the Selenium Python bindings to the project, it's time to import Selenium's WebDriver and some of its helpful configuration options to the actual Python script that does the website scraping. I named my file &lt;code&gt;for_sale_scraper.py&lt;/code&gt; since I was specifically looking for NFTs that are for sale (not all of the NFTs listed on NFTrade are - some are just visible but not actually available to purchase), but you can choose any sort of file name that makes sense for you.&lt;/p&gt;

&lt;p&gt;Below are the imports I added to my file. I'll break down what each one is doing below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;for_sale_scraper.py&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.chrome.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Options&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.support.wait&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebDriverWait&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.common.by&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;By&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The very first import line brings in the &lt;code&gt;selenium.webdriver&lt;/code&gt; module and provides all the WebDriver implementations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, as I chose to use Chrome as the browser I wanted WebDriver to interact with (Selenium supports Firefox, Chrome, Edge, and Safari browsers), I imported the &lt;code&gt;Options&lt;/code&gt; class from the &lt;a href="https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.chrome.webdriver" rel="noopener noreferrer"&gt;&lt;code&gt;selenium.webdriver.chrome.options&lt;/code&gt; module&lt;/a&gt;. This allowed me to add specific config details about how I want the Chrome browser to be set up when the Python script runs against it: things like headless mode or disable extensions, etc.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.chrome.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Options&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'll cover the arguments I passed here in detail in the next section.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.support.wait" rel="noopener noreferrer"&gt;&lt;code&gt;WebDriverWait&lt;/code&gt;&lt;/a&gt;, added in the third line of imports, is part of the special sauce that makes WebDriver a good solution for sites like NFTrade that dynamically fetch data on the client side: it allows for &lt;a href="https://selenium-python.readthedocs.io/waits.html" rel="noopener noreferrer"&gt;implicit and explicit wait times&lt;/a&gt; before trying to locate an element on the page, which gives the browser time for data to come back from the server and populate in the DOM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.support.wait&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebDriverWait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This type of wait is an "explicit wait", meaning I manually set a period during which the code will wait before continuing to try and execute.&lt;/p&gt;

&lt;p&gt;And finally, there is the import for &lt;code&gt;By&lt;/code&gt;. &lt;code&gt;By&lt;/code&gt; is what allows me to &lt;a href="https://selenium-python.readthedocs.io/locating-elements.html" rel="noopener noreferrer"&gt;locate elements&lt;/a&gt; on the page - it is immeasurably useful and powerful.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://selenium-python.readthedocs.io/api.html#locate-elements-by" rel="noopener noreferrer"&gt;&lt;code&gt;By&lt;/code&gt;&lt;/a&gt; class accepts element IDs, names, attributes, XPaths, link text, tag names, class names, and CSS selectors just to name a few, and once again, it is a key player when it comes to scraping data off of the web page, as I'll demonstrate soon.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.common.by&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;By&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right, all the Selenium WebDriver imports are now present in the Python file, time to initialize them and get to work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add methods to scrape data and lazy load more data
&lt;/h3&gt;

&lt;p&gt;Before WebDriver can begin scraping the data from NFTrade, an instance of the browser that WebDriver will interact with must be instantiated and the proper options supplied to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Initialize the Selenium WebDriver instance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my attempt to try to follow good Python coding practices (again, disclaimer: I don't write Python as my primary coding language), I created a class for the the file named &lt;code&gt;class ForSaleNFTScraper&lt;/code&gt;, and created an &lt;code&gt;__init__ ()&lt;/code&gt; method immediately inside of it where I created the Chrome WebDriver instance that the whole script will be able reference in the remainder of its methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ForSaleNFTScraper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--headless&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--start-maximized&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WebDriverWait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# more code here
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first thing I did inside of the &lt;code&gt;__init__ ()&lt;/code&gt; method was to add a couple of Chrome browser configs via the &lt;code&gt;Options&lt;/code&gt; import from the last section by declaring a new &lt;code&gt;options&lt;/code&gt; variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since I wanted this script to run without actually opening a browser window on the user's local machine, I added the config argument of &lt;code&gt;--headless&lt;/code&gt; and the argument of &lt;code&gt;--start-maximized&lt;/code&gt;, so the (unseen) window would take up as much screen size as was available (and hopefully load as many NFTs as quickly as possible by doing so).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--headless&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--start-maximized&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I passed the new &lt;code&gt;options&lt;/code&gt; object to the instance of &lt;code&gt;webdriver.Chrome&lt;/code&gt;, which was set to the variable of &lt;code&gt;self.driver&lt;/code&gt; (&lt;code&gt;self&lt;/code&gt; is a variable accessible throughout the rest of the methods within this &lt;code&gt;ForSaleNFTScraper&lt;/code&gt; class), and instructed the new WebDriver to wait for 5 seconds after startup (which would presumably give it time to go to the specified NFTrade web URL and load the data onto the page before attempting to scrape it).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WebDriverWait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's plenty happening in that first method, but it's all pretty straightforward once you go through the code line by line and understand what the arguments mean to the Chrome WebDriver instance, and why it's doing what it's doing. Now that the WebDriver instance was configured and ready to go, I could write the code fetching the NFT card data, and lazy loading more data once the end of the currently visible info was reached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Write the &lt;code&gt;get_cards()&lt;/code&gt; and &lt;code&gt;get_current_card_count()&lt;/code&gt; methods&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where the code really starts to get interesting in my opinion, because it's where I learned to collect whatever data was currently visible in a (headless) browser &lt;em&gt;and then&lt;/em&gt; load more data to add to the list. Pay close attention, because this is where the lazy loading code resides that gets more and more data onto the page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_current_card_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get the count of cards loaded into list of cards.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XPATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;//div[contains(@class, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item_itemContent__1XIcH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;    

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_card_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Extract and returns card ID and price.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://nftrade.com/collection/[NFT_COLLECTION_NAME]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;last_card_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="c1"&gt;# loops through lazy loading cards on site until max_card_count number reached 
&lt;/span&gt;    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;last_card_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_card_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window.scrollTo(0, document.body.scrollHeight);&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;last_card_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_card_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# grab all the cards now loaded into the browser by XPATH
&lt;/span&gt;    &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XPATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;//div[contains(@class, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item_itemContent__1XIcH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;

&lt;span class="c1"&gt;# more code here        
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, here we go.&lt;/p&gt;

&lt;p&gt;For starters, there are two methods that I'm displaying in the code snippet here. The first method, &lt;code&gt;get_current_card_count()&lt;/code&gt; is how I keep track of how many NFT cards in a collection are currently visible on the screen.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As I've said, NFTrade lazy loads its NFT collections onto a site to make initial page load quicker, and when a user scrolls down to the end of the currently loaded batch of elements, the NFTrade page then triggers to load more cards into the DOM at that point in time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second method is &lt;code&gt;get_cards()&lt;/code&gt;, which handles going to the NFTrade collection URL and scraping all the available card data. It relies on &lt;code&gt;get_current_card_count()&lt;/code&gt; to help it know to load more NFT cards until the desired number of cards has been loaded in the browser to scrape data from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;get_cards()&lt;/code&gt; method&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'll talk about &lt;code&gt;get_cards()&lt;/code&gt; first as it's the more complicated of the two methods. The first thing the method does is declare a new variable named &lt;code&gt;URL&lt;/code&gt; - this variable is set to the URL of the NFTrade collection page I want WebDriver to navigate to and scrape the data from. I used the Selenium WebDriver &lt;a href="https://selenium-python.readthedocs.io/api.html#selenium.webdriver.remote.webdriver.WebDriver.get" rel="noopener noreferrer"&gt;&lt;code&gt;driver.get()&lt;/code&gt; method&lt;/a&gt; to navigate to the page given by the URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://nftrade.com/collection/[NFT_COLLECTION_NAME]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
   &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After navigating to the proper URL, I created a variable called &lt;code&gt;last_card_count&lt;/code&gt; and set it equal to 0: this variable will be used to track how many NFTs are currently visible on the page and compare it to the &lt;code&gt;max_card_count&lt;/code&gt; variable passed to the &lt;code&gt;get_cards()&lt;/code&gt; method (if a number isn't passed for &lt;code&gt;max_card_count&lt;/code&gt; it defaults to 500).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Below is the key code to lazy loading more and more data in the browser&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inside of &lt;code&gt;get_cards()&lt;/code&gt;, there's a &lt;code&gt;while&lt;/code&gt; loop set up to compare the &lt;code&gt;last_card_count&lt;/code&gt; and &lt;code&gt;max_card_count&lt;/code&gt; variables. As long as &lt;code&gt;last_card_count&lt;/code&gt; is less then &lt;code&gt;max_card_count&lt;/code&gt;, the loop will run, and each time it executes WebDriver uses the &lt;a href="https://selenium-python.readthedocs.io/api.html#selenium.webdriver.remote.webdriver.WebDriver.execute_script" rel="noopener noreferrer"&gt;&lt;code&gt;driver.execute_script()&lt;/code&gt; method&lt;/a&gt; to scroll down the page, wait for 3 seconds (allowing more cards to load onscreen), and then updating the &lt;code&gt;last_card_count&lt;/code&gt; variable equal to the new amount of cards on the page using the &lt;code&gt;get_current_card_count()&lt;/code&gt; method.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The &lt;code&gt;window.scrollTo()&lt;/code&gt; method is critical&lt;/p&gt;

&lt;p&gt;&lt;code&gt;driver.execute_script()&lt;/code&gt; allows for the synchronous execution of JavaScript in the current window, so when you see the code &lt;code&gt;self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")&lt;/code&gt;, what's happening is that WebDriver is using the JavaScript &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo" rel="noopener noreferrer"&gt;&lt;code&gt;window.scrollTo()&lt;/code&gt; method&lt;/a&gt; to scroll the browser all the way to the bottom of the page (that's why &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight" rel="noopener noreferrer"&gt;&lt;code&gt;document.body.scrollHeight&lt;/code&gt;&lt;/a&gt; is present - it's a measurement of the height of the whole &lt;code&gt;document.body&lt;/code&gt; page element), which triggers the page to load more NFT cards into view.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;last_card_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="c1"&gt;# loops through lazy loading cards on site until max_card_count number reached 
&lt;/span&gt;    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;last_card_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_card_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window.scrollTo(0, document.body.scrollHeight);&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;last_card_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_card_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is a perfect time to segue into discussing the &lt;code&gt;get_current_card_count()&lt;/code&gt; method, which is short and sweet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;get_current_card_count()&lt;/code&gt; method&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This method exists simply to find the count of the current elements loaded in the browser, and it does so by combining the WebDriver &lt;a href="https://selenium-python.readthedocs.io/api.html#selenium.webdriver.remote.webdriver.WebDriver.find_elements" rel="noopener noreferrer"&gt;&lt;code&gt;find_elements()&lt;/code&gt; method&lt;/a&gt; with the &lt;a href="https://selenium-python.readthedocs.io/api.html#locate-elements-by" rel="noopener noreferrer"&gt;&lt;code&gt;By.XPATH&lt;/code&gt; element locator&lt;/a&gt; method.&lt;/p&gt;

&lt;p&gt;Due to how the NFTrade site is built, there are no easily identifiable classes, IDs, or other consistent ways to identify all the cards on the page, so I had to resort to XPath expressions to identify each element and include it in my count to update the &lt;code&gt;last_card_count&lt;/code&gt; variable. I cobbled together the XPath below by using my Chrome DevTools to inspect the elements on the page and construct the XPath from there through trial and error.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; What is XPath?&lt;/p&gt;

&lt;p&gt;If you're unfamiliar like I was, &lt;a href="https://www.w3schools.com/xml/xml_xpath.asp" rel="noopener noreferrer"&gt;XPath&lt;/a&gt; is a syntax that can be used to navigate through elements and attributes in a standard XML document (or webpage). The link I provided to W3Schools has some good examples of what typical XPath expressions look like and how to interpret them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the code inside of the &lt;code&gt;get_current_card_count()&lt;/code&gt; method is just the one line of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XPATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;//div[contains(@class, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item_itemContent__1XIcH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the code snippet, I'm getting the count (using the build-in &lt;a href="https://www.w3schools.com/python/ref_func_len.asp" rel="noopener noreferrer"&gt;Python method &lt;code&gt;len()&lt;/code&gt;&lt;/a&gt;) of all the elements on the page that match the XPath of a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; containing the class of &lt;code&gt;"Item_itemContent__1XIcH"&lt;/code&gt;, because each NFT on the page is wrapped by that &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with that class. It's not the prettiest thing to read and understand, but it gets the job done.&lt;/p&gt;

&lt;p&gt;And finally, jumping back to the &lt;code&gt;get_cards()&lt;/code&gt; method again, once the &lt;code&gt;last_card_count&lt;/code&gt; variable has been updated and surpassed the &lt;code&gt;max_card_count&lt;/code&gt; variable (i.e. enough NFT cards are loaded into the browser), the &lt;code&gt;while&lt;/code&gt; loop ends, and all the cards on the screen are targeted (using the very same XPath used in the &lt;code&gt;get_current_card_count()&lt;/code&gt; method, I might add) and set equal to the &lt;code&gt;cards&lt;/code&gt; variable defined at the top of the &lt;code&gt;get_cards()&lt;/code&gt; method. That variable then gets returned to the &lt;code&gt;__main__&lt;/code&gt; method running the whole script, which I'll cover next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# grab all the cards now loaded into the browser by XPATH
&lt;/span&gt;    &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XPATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;//div[contains(@class, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item_itemContent__1XIcH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's quite a bit going on here, but hopefully it makes more sense now what these methods are doing. Time to test out this lazy loading script functionality and see how WebDriver does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Run the Python script&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All right, now that all the code and logic to load multiple sets of NFTrade cards into the browser and collect the data has been written, it's time to run the code.&lt;/p&gt;

&lt;p&gt;To do that, I declared a &lt;code&gt;__main__&lt;/code&gt; method at the bottom of the file which can be started from the terminal with the following command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python for_sale_scraper.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what the &lt;code&gt;__main__&lt;/code&gt; method includes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; __main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;scraper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ForSaleNFTScraper&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;# get all the cards from nftrade site
&lt;/span&gt;   &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_card_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# pprint the card data to ensure we're getting data
&lt;/span&gt;   &lt;span class="nf"&gt;pprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total cards collected:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
   &lt;span class="c1"&gt;# more code here
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first thing the method does is create a new instantiation variable named &lt;code&gt;scraper&lt;/code&gt; by calling the &lt;code&gt;ForSaleNFTScraper()&lt;/code&gt; class. It then proceeds to fetch all the card data and set it equal to a variable named &lt;code&gt;cards&lt;/code&gt; by calling the method &lt;code&gt;scraper.get_cards(max_card_count=200)&lt;/code&gt; and supplying a &lt;code&gt;max_card_count&lt;/code&gt; variable of 200.&lt;/p&gt;

&lt;p&gt;After this step, as a sanity check, I used the Python &lt;a href="https://docs.python.org/3/library/pprint.html" rel="noopener noreferrer"&gt;&lt;code&gt;pprint()&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://www.w3schools.com/python/ref_func_print.asp" rel="noopener noreferrer"&gt;&lt;code&gt;print()&lt;/code&gt;&lt;/a&gt; methods to print out all the card data and a count of the total cards fetched by the &lt;code&gt;get_cards()&lt;/code&gt; method, and ensure all the info I needed to include in the CSV (NFT price, NFT ID, etc.) was available to me. Here's a screenshot of some of the data printed out in my console helping me know my code is doing what I expect.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/3026b05e06f81c86921ebaf1caaaabda/47aef/pprint-nft-cards.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F3026b05e06f81c86921ebaf1caaaabda%2F1e043%2Fpprint-nft-cards.png" title="Example of the raw NFT data gathered from the get\_cards() method" alt="Example of the raw NFT data gathered from the get_cards() method" width="690" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Here is what the raw NFT card data gathered from the &lt;code&gt;get_cards()&lt;/code&gt; method looks like printed in the terminal.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/39ed438f6bbdcfb15a88985cdbee53e4/db3a5/print-nft-cards-length.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F39ed438f6bbdcfb15a88985cdbee53e4%2F1e043%2Fprint-nft-cards-length.png" title="Count of the amount of NFTs collected from the get\_cards() method" alt="Count of the amount of NFTs collected from the get_cards() method" width="690" height="33"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Since I set my &lt;code&gt;max_card_count&lt;/code&gt; to 200, but NFTrade loads NFTs in batches of 75 at a time, it makes sense that the total count of NFTs scraped off the page equals 225.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And after verifying the right data's there (and the right amount of data as well), I continued on extracting the data, calculating the current price in USD for each NFT, and assembling a CSV of all the data. But I'll save those steps for future blog posts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building a Python-based website scraper to create a CSV of NFTs available for sale on NFTrade was a unique challenge I learned a lot of new things from.&lt;/p&gt;

&lt;p&gt;After my first attempt failed due to NFTrade dynamically lazy loading NFTs in batches of 75 onto the page as a user scrolled further down, I had to come up with a more creative solution that would allow me to trigger the site to load more cards on the page first, then grab the data on the cards for sale.&lt;/p&gt;

&lt;p&gt;I found the solution I was looking for with the help of a Python package called Selenium Python. Selenium Python is a powerful Python-based API that allows users to write scripts or automated tests leveraging Selenium WebDriver. And it was up to the task at hand: with just a few methods I was able to specify as many NFTs as I wanted loaded on the page before scraping and collecting all their data all at once.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more blogs about the problems I had to solve while building this Python website scraper in addition to other topics on JavaScript or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you’d like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com" rel="noopener noreferrer"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope seeing how to make a Python Selenium WebDriver load data onto a dynamic webpage before scraping it comes in handy for you in the future.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further References &amp;amp; Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://selenium-python.readthedocs.io/" rel="noopener noreferrer"&gt;Selenium Python&lt;/a&gt; docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.selenium.dev/documentation/webdriver/" rel="noopener noreferrer"&gt;Selenium WebDriver&lt;/a&gt; docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nftrade.com/" rel="noopener noreferrer"&gt;NFTrade&lt;/a&gt; website&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.w3schools.com/xml/xml_xpath.asp" rel="noopener noreferrer"&gt;XPath&lt;/a&gt; documentation&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo" rel="noopener noreferrer"&gt;JavaScript window.scrollTo() method&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight" rel="noopener noreferrer"&gt;JavaScript document.body.scrollHeight&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>selenium</category>
      <category>bigdata</category>
      <category>webdriver</category>
    </item>
    <item>
      <title>Copy Files from One Repo to Another Automatically with GitHub Actions</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Sun, 13 Aug 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/paigen11/copy-files-from-one-repo-to-another-automatically-with-github-actions-k11</link>
      <guid>https://dev.to/paigen11/copy-files-from-one-repo-to-another-automatically-with-github-actions-k11</guid>
      <description>&lt;p&gt;&lt;a href="/static/bb9a4c12a3ec59c59c10ac75f4aaa89b/5a190/assembly-line-hero.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2Fbb9a4c12a3ec59c59c10ac75f4aaa89b%2F1e043%2Fassembly-line-hero.png" title="Assembly line making identical pink shoes" alt="Assembly line making identical pink shoes" width="690" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Welcome to the third (and final) installment in my &lt;a href="https://www.paigeniedringhaus.com/blog/use-git-hub-actions-to-automatically-publish-a-repo-subfolder-as-an-npm-library" rel="noopener noreferrer"&gt;series&lt;/a&gt; about the many things I learned while building my first open source API library for &lt;a href="https://blues.io/?&amp;amp;utm_source=dev.to&amp;amp;utm_medium=web&amp;amp;utm_campaign=nf&amp;amp;utm_content=notehub-js"&gt;Blues&lt;/a&gt;, the Internet of Things startup I work for.&lt;/p&gt;

&lt;p&gt;It was a good challenge and a great learning opportunity for me.&lt;/p&gt;

&lt;p&gt;I automated as many pieces of the library's maintenance and deployment to as I could with the help of GitHub Actions workflows, one of them being: copy the &lt;code&gt;openapi.yaml&lt;/code&gt; file from the Blues cloud repo, &lt;a href="https://notehub.io/?&amp;amp;utm_source=dev.to&amp;amp;utm_medium=web&amp;amp;utm_campaign=nf&amp;amp;utm_content=notehub-js"&gt;Notehub&lt;/a&gt;, whenever that file was updated, and push it into a feature branch in the &lt;a href="https://github.com/blues/notehub-js" rel="noopener noreferrer"&gt;Notehub JS repo&lt;/a&gt; (the name of the API library).&lt;/p&gt;

&lt;p&gt;Interacting with two different repos in one workflow might sound tricky, and at first it was, but it &lt;em&gt;is&lt;/em&gt; possible and not too bad once you see how it can be done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In this tutorial, I'll demonstrate how to create a GitHub Actions workflow that automatically copies a file from one repo to another and pushes the changes to another repo inside a feature branch.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I know the use case sounds a bit specific, but it came in really handy for me, and you never know when it might for you too.&lt;/p&gt;




&lt;h2&gt;
  
  
  Notehub JS
&lt;/h2&gt;

&lt;p&gt;Before we get to the actual GitHub Actions workflows, I'll give you a little background on the &lt;a href="https://github.com/blues/notehub-js" rel="noopener noreferrer"&gt;&lt;strong&gt;Notehub JS&lt;/strong&gt;&lt;/a&gt; project.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This section outlines the folder structure for the repo in case you want to explore it in GitHub, if you just want to get to the solutions, feel free to jump down to the next section.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notehub JS is a JavaScript-based library for interacting with the native &lt;a href="https://dev.blues.io/reference/notehub-api/api-introduction/?&amp;amp;utm_source=dev.to&amp;amp;utm_medium=web&amp;amp;utm_campaign=nf&amp;amp;utm_content=notehub-js"&gt;&lt;strong&gt;Notehub API&lt;/strong&gt;&lt;/a&gt;, and it is actually generated from the Notehub project's own &lt;code&gt;openapi.yaml&lt;/code&gt; file, which follows the &lt;a href="https://swagger.io/specification/" rel="noopener noreferrer"&gt;OpenAPI specification&lt;/a&gt; standards.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://openapi-generator.tech/docs/installation" rel="noopener noreferrer"&gt;OpenAPI Generator CLI&lt;/a&gt; is a tool that uses the &lt;code&gt;openapi.yaml&lt;/code&gt; file to create an entire API library complete with documentation, models, endpoints, and scripts to package it up for publishing as an npm module. The end library that is published to npm is a &lt;em&gt;subfolder&lt;/em&gt; inside of the main Notehub JS repo. At the root of the project are the &lt;code&gt;openapi.yaml&lt;/code&gt; file, the GitHub Actions workflows, and a few other config files.&lt;/p&gt;

&lt;p&gt;Here's a simplified view of the &lt;a href="https://github.com/blues/notehub-js" rel="noopener noreferrer"&gt;Notehub JS repo's&lt;/a&gt; folder structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root/
├── .github/ 
| ├── workflows/
| | ├── create-pr.yml 
| ├── PULL_REQUEST_TEMPLATE.md
├── src/ &amp;lt;- this is the folder generated by the OpenAPI Generator CLI
| ├── dist/
| ├── docs/
| ├── src/ 
| | ├── api/ 
| | ├── model/
| | ├── index.js 
| openapi.yaml
| config.json
| package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Based on the diagram above, the &lt;code&gt;openapi.yaml&lt;/code&gt; file that the library is generated from lives at the root of the repo, and the &lt;code&gt;src/&lt;/code&gt; folder is what holds all the Notehub API JavaScript code that powers the &lt;a href="https://www.npmjs.com/package/@blues-inc/notehub-js" rel="noopener noreferrer"&gt;Notehub JS library&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;openapi.yaml&lt;/code&gt; file gets copied from the Notehub repo when changes are made to it. And changes are made to that file whenever the Notehub API is updated with new features and functionality, so making sure that the Notehub JS library keeps up with the new additions to the original API it's based on is important.&lt;/p&gt;

&lt;p&gt;Now that I've explained more about the Notehub JS repo and why keeping it in sync with the Notehub API is important (and depends on the &lt;code&gt;openapi.yaml&lt;/code&gt; file), we can get down to the business of ensuring the file gets copied to this repo whenever it's updated.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Looking for more details about Notehub JS?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check out my &lt;a href="https://www.paigeniedringhaus.com/blog/use-git-hub-actions-to-automatically-publish-a-repo-subfolder-as-an-npm-library" rel="noopener noreferrer"&gt;first blog post&lt;/a&gt; about how to use GitHub Actions to automatically publish new releases to npm - I do a fairly deep dive on Notehub JS there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create a GitHub Actions workflow to run when the &lt;code&gt;openapi.yaml&lt;/code&gt; file is updated
&lt;/h3&gt;

&lt;p&gt;First things first: creating a new GitHub Actions workflow that's triggered when the &lt;code&gt;openapi.yaml&lt;/code&gt; file changes in the Notehub repo.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Need a refresher on GitHub Actions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want a quick primer on what GitHub Actions are, I recommend you check out a previous article I wrote about them &lt;a href="https://www.paigeniedringhaus.com/blog/use-secret-environment-variables-in-git-hub-actions#what-is-github-actions" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Below is the final version of the &lt;code&gt;copy-openapi-file.yml&lt;/code&gt; file, which lives inside of the &lt;code&gt;./github/workflows/&lt;/code&gt; folder. I'll walk through each step of the script below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.github/workflows/copy-openapi-file.yml&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Copy updated OpenAPI file&lt;/span&gt;

&lt;span class="c1"&gt;# only run this workflow when the openapi file has changed on the master branch&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notehub/api/openapi.yaml'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;copy_openapi_file_to_notehub_js&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# check out notehub project&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out Notehub project&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
    &lt;span class="c1"&gt;# check out notehub-js project using token generated in that project to successfully access it from inside the Notehub GitHub Action workflow&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out Notehub JS project&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blues/notehub-js&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./notehub-js&lt;/span&gt;
        &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NOTEHUB_JS_TOKEN }}&lt;/span&gt;
    &lt;span class="c1"&gt;# make a copy the openapi file from notehub project&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Copy OpenAPI file&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash ./.github/scripts/copy-openapi-file.sh&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;DESTINATION_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./notehub-js/&lt;/span&gt;
    &lt;span class="c1"&gt;# make a branch in notehub-js repo and push the copy of the openapi file there&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Push to notehub-js repo&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash ./.github/scripts/push-openapi-to-notehub-js.sh&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;BRANCH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;feat-openapi-update&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each GitHub Actions workflow needs a &lt;strong&gt;&lt;code&gt;name&lt;/code&gt;&lt;/strong&gt; , and I try to choose descriptive ones like this one: &lt;code&gt;Copy updated OpenAPI file&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then, I want this workflow to only run when the &lt;code&gt;openapi.yaml&lt;/code&gt; file in the &lt;code&gt;master&lt;/code&gt; branch of the project is changed. &lt;code&gt;master&lt;/code&gt; is the branch that gets deployed to production, and in my case it's a safe assumption that when there's a change to this file in &lt;code&gt;master&lt;/code&gt;, those changes are going to prod, which is when I want to copy the file over to the Notehub JS repo so it can reflect those same changes.&lt;/p&gt;

&lt;p&gt;The code snippet below does just that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notehub/api/openapi.yaml'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;on&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is how a workflow is triggered.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpushbranchestagsbranches-ignoretags-ignore" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;push&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is the event that triggers the workflow.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-including-branches-and-tags" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;branches&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; defines which branch names this workflow should run for.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-including-paths" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;paths&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is another filter to even more finely determine when the workflow runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it boils down to is: when there's a change to the &lt;code&gt;master&lt;/code&gt; branch in the repo for the &lt;code&gt;openapi.yaml&lt;/code&gt; file at this particular file path, run the GitHub Actions workflow.&lt;/p&gt;

&lt;p&gt;From there, the &lt;code&gt;copy_openapi_file_to_notehub_js&lt;/code&gt; job runs. There's only one job in the &lt;strong&gt;&lt;code&gt;jobs&lt;/code&gt;&lt;/strong&gt; section of this script, but if there were multiple jobs, they'll run sequentially.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;create_pr_repo_sync&lt;/code&gt; job defines that it runs on the latest version of Ubuntu in &lt;strong&gt;&lt;code&gt;runs-on&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And finally, I get to the &lt;strong&gt;&lt;code&gt;steps&lt;/code&gt;&lt;/strong&gt; section. Here's how it goes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check out the Notehub code so the workflow can access it using the GitHub Action &lt;a href="https://github.com/actions/checkout" rel="noopener noreferrer"&gt;&lt;code&gt;actions/checkout@v3&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Check out the Notehub JS code so the workflow can access it as well using the GitHub Action &lt;a href="https://github.com/actions/checkout" rel="noopener noreferrer"&gt;&lt;code&gt;actions/checkout@v3&lt;/code&gt;&lt;/a&gt;.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NOTE&lt;/strong&gt; : Notice that in this second checkout action I had to include a &lt;a href="https://github.com/actions/checkout#checkout-multiple-repos-side-by-side" rel="noopener noreferrer"&gt;&lt;code&gt;with&lt;/code&gt;&lt;/a&gt; parameter that allows me to specify another repo to check out by providing a &lt;code&gt;repository&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, and &lt;code&gt;token&lt;/code&gt;. The &lt;code&gt;token&lt;/code&gt; is a GitHub &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" rel="noopener noreferrer"&gt;Personal Access Token (PAT)&lt;/a&gt; required when checking out other libraries beyond the current one the workflow is operating within.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Run a shell script named &lt;code&gt;copy-openapi-file.sh&lt;/code&gt; with the &lt;a href="https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow" rel="noopener noreferrer"&gt;GitHub Actions environment variable&lt;/a&gt; &lt;code&gt;DESTINATION_PATH&lt;/code&gt; passed to the script.&lt;/li&gt;
&lt;li&gt;Run another shell script named &lt;code&gt;push-openapi-to-notehub-js.sh&lt;/code&gt; with the &lt;a href="https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow" rel="noopener noreferrer"&gt;GitHub Actions environment variable&lt;/a&gt; &lt;code&gt;BRANCH&lt;/code&gt; passed to the script.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the following sections I'll go through what those two shell scripts do in detail.&lt;/p&gt;

&lt;p&gt;Ok, so the workflow file is done. Time to get into the details of the two scripts doing the heavy lifting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write one shell script to copy the &lt;code&gt;openapi.yaml&lt;/code&gt; file to the Notehub JS repo
&lt;/h3&gt;

&lt;p&gt;Premade GitHub Actions can do many things, but something as specific as copying a file from one repo to another is a bit beyond them.&lt;/p&gt;

&lt;p&gt;Luckily, GitHub Action workflows can &lt;a href="https://docs.github.com/en/actions/learn-github-actions/essential-features-of-github-actions#adding-scripts-to-your-workflow" rel="noopener noreferrer"&gt;run shell scripts&lt;/a&gt;, so if you can use a scripting language like PowerShell or Bash, you're in luck.&lt;/p&gt;

&lt;p&gt;Here is the Bash script I wrote to copy the file from the Notehub repo to the Notehub JS repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;copy_openapi_file.sh&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# bash boilerplate&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail &lt;span class="c"&gt;# strict mode&lt;/span&gt;
&lt;span class="nb"&gt;readonly &lt;/span&gt;&lt;span class="nv"&gt;SCRIPT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;readonly &lt;/span&gt;&lt;span class="nv"&gt;SCRIPT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;l &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="c"&gt;# Log a message to the terminal.&lt;/span&gt;
    &lt;span class="nb"&gt;echo
    echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_NAME&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# File to copy from Notehub&lt;/span&gt;
&lt;span class="nv"&gt;OPENAPI_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./notehub/api/openapi.yaml

&lt;span class="c"&gt;# if the file exists in Notehub, copy it to Notehub-JS repo&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OPENAPI_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Copying &lt;/span&gt;&lt;span class="nv"&gt;$OPENAPI_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; ./notehub/api/openapi.yaml &lt;span class="nv"&gt;$DESTINATION_PATH&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OpenAPI file copied to &lt;/span&gt;&lt;span class="nv"&gt;$DESTINATION_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 9 lines at the top of the file are Bash script set up, so don't worry too much about what's happening there.&lt;/p&gt;

&lt;p&gt;The first line to pay attention to is where the &lt;code&gt;OPENAPI_FILE&lt;/code&gt; variable is defined with a path to the &lt;code&gt;openapi.yaml&lt;/code&gt; file in the Notehub repo.&lt;/p&gt;

&lt;p&gt;After identifying the file to copy from the Notehub repo, a standard &lt;a href="https://tldp.org/LDP/Bash-Beginners-Guide/html/sect_07_01.html" rel="noopener noreferrer"&gt;Bash &lt;code&gt;if&lt;/code&gt; statement&lt;/a&gt; checks if the file exists in the Notehub repo (just to be safe), and if it does, it copies the file recursively using &lt;code&gt;cp -R&lt;/code&gt; to the &lt;code&gt;$DESTINATION_PATH&lt;/code&gt; variable that was provided by the GitHub Actions environment variable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this case, the &lt;code&gt;$DESTINATION_PATH&lt;/code&gt; is pointing to the root folder of the Notehub JS project where the file should be copied to, but you could specify it copy the file anywhere within the receiving repo that makes sense.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;During the copying process and when the script finishes copying the file I added a few &lt;a href="https://ss64.com/bash/echo.html" rel="noopener noreferrer"&gt;&lt;code&gt;echo&lt;/code&gt; commands&lt;/a&gt; to print out useful messages in the logs to let me know things are going to plan.&lt;/p&gt;

&lt;p&gt;And that's the first shell script that actually copies the file that will be taken from one repo to the other. I hope it's not too complicated once the actions in the script are broken down.&lt;/p&gt;

&lt;p&gt;Now, it's time to add that file to the Notehub JS repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use a second shell script to push the copied file to a feature branch in the other repo
&lt;/h3&gt;

&lt;p&gt;When I checked the &lt;a href="https://github.com/marketplace" rel="noopener noreferrer"&gt;GitHub Marketplace&lt;/a&gt; for a ready-made action to create a new feature branch in the repo receiving the copied file, there were a few to choose from, but I wanted more control over what was happening than they offered.&lt;/p&gt;

&lt;p&gt;I wanted an action that would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new branch in the Notehub JS receiving repo if it didn't exist, add the copied file, and then push that branch to GitHub.&lt;/li&gt;
&lt;li&gt;Check out the already existing branch in the Notehub JS repo, overwrite any previous file changes in the branch, and then push the new file changes in that branch back to GitHub.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the bash script that I came up with to handle those two scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;push-openapi-to-notehub-js.sh&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# bash boilerplate&lt;/span&gt;
&lt;span class="nb"&gt;readonly &lt;/span&gt;&lt;span class="nv"&gt;SCRIPT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;readonly &lt;/span&gt;&lt;span class="nv"&gt;SCRIPT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;l &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="c"&gt;# Log a message to the terminal.&lt;/span&gt;
    &lt;span class="nb"&gt;echo
    echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_NAME&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# move to the root the notehub-js repo&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"./notehub-js"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Open root of Notehub JS repo"&lt;/span&gt;

&lt;span class="c"&gt;# check if there's already a currently existing feature branch in notehub-js for this branch&lt;/span&gt;
&lt;span class="c"&gt;# i.e. the altered openapi.yaml file's already been copied there at least once before&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Check if feature branch &lt;/span&gt;&lt;span class="nv"&gt;$BRANCH&lt;/span&gt;&lt;span class="s2"&gt; already exists in Notehub JS"&lt;/span&gt;
git ls-remote &lt;span class="nt"&gt;--exit-code&lt;/span&gt; &lt;span class="nt"&gt;--heads&lt;/span&gt; origin &lt;span class="nv"&gt;$BRANCH&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1
&lt;span class="nv"&gt;EXIT_CODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"EXIT CODE &lt;/span&gt;&lt;span class="nv"&gt;$EXIT_CODE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="nv"&gt;$EXIT_CODE&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Git branch '&lt;/span&gt;&lt;span class="nv"&gt;$BRANCH&lt;/span&gt;&lt;span class="s2"&gt;' exists in the remote repository"&lt;/span&gt;
  &lt;span class="c"&gt;# fetch branches from notehub-js&lt;/span&gt;
  git fetch
  &lt;span class="c"&gt;# stash currently copied openapi.yaml&lt;/span&gt;
  git stash
  &lt;span class="c"&gt;# check out existing branch from notehub-js&lt;/span&gt;
  git checkout &lt;span class="nv"&gt;$BRANCH&lt;/span&gt; 
  &lt;span class="c"&gt;# overwrite any previous openapi.yaml changes with current ones&lt;/span&gt;
  git checkout stash &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Git branch '&lt;/span&gt;&lt;span class="nv"&gt;$BRANCH&lt;/span&gt;&lt;span class="s2"&gt;' does not exist in the remote repository"&lt;/span&gt;
  &lt;span class="c"&gt;# create a new branch in notehub-js &lt;/span&gt;
  git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nv"&gt;$BRANCH&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;git add &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
git config user.name github-actions
git config user.email github-actions@github.com
git commit &lt;span class="nt"&gt;-am&lt;/span&gt; &lt;span class="s2"&gt;"feat: Update OpenAPI file replicated from Notehub"&lt;/span&gt;
git push &lt;span class="nt"&gt;--set-upstream&lt;/span&gt; origin &lt;span class="nv"&gt;$BRANCH&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Updated OpenAPI file successfully pushed to notehub-js repo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once more, the first 9 lines of code are boilerplate for Bash; no comments necessary.&lt;/p&gt;

&lt;p&gt;The first thing this script does is situate itself in the root of the Notehub JS folder just as you would when navigating around a computer via the command line: &lt;code&gt;cd "./notehub-js"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once there, it checks if the feature branch environment variable passed in by the GitHub Action &lt;code&gt;$BRANCH&lt;/code&gt; already exists in the Notehub JS repo with this one-liner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git ls-remote &lt;span class="nt"&gt;--exit-code&lt;/span&gt; &lt;span class="nt"&gt;--heads&lt;/span&gt; origin &lt;span class="nv"&gt;$BRANCH&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essentially, this code is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Getting a list of references in the remote repository with &lt;code&gt;git ls-remote&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;Checking if env var &lt;code&gt;$BRANCH&lt;/code&gt; (passed from the GitHub Action) exists &lt;em&gt;only&lt;/em&gt; in the &lt;code&gt;refs/heads&lt;/code&gt; of the origin remote with &lt;code&gt;--heads&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;(i.e. does the branch name already exist in the GitHub repo where it keeps track of the names of all of its branches),&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;And returning an &lt;code&gt;--exit-code&lt;/code&gt; with status &lt;code&gt;"2"&lt;/code&gt; when no matching refs are found in the remote repo and a status of &lt;code&gt;"0"&lt;/code&gt; if a matching ref is found.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;When the exit code is returned, it's set equal to the variable &lt;code&gt;$EXIT_CODE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then, an &lt;code&gt;if/else&lt;/code&gt; statement checks what &lt;code&gt;$EXIT_CODE&lt;/code&gt; equals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EXIT_CODE = "0"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the exit code is &lt;code&gt;0&lt;/code&gt;, that means the branch already exists in the repository; this could happen if multiple changes are made to the &lt;code&gt;openapi.yaml&lt;/code&gt; file before the feature branch in Notehub JS gets merged in and deleted.&lt;/p&gt;

&lt;p&gt;When that happens the following steps take place:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The branches for the Notehub JS repo are fetched via &lt;code&gt;git fetch&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;The current changes (the freshly copied &lt;code&gt;openapi.yaml&lt;/code&gt; file) are stashed with &lt;code&gt;git stash&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;The existing branch of the same name as the &lt;code&gt;$BRANCH&lt;/code&gt; env var is checked out locally,&lt;/li&gt;
&lt;li&gt;The stashed changes are applied to that branch overwriting whatever was there before with &lt;code&gt;git checkout stash -- .&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;EXIT_CODE = "2"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;$EXIT_CODE&lt;/code&gt; is equal to &lt;code&gt;2&lt;/code&gt;, that means the branch doesn't already exist remotely in GitHub and the script can simply checkout a new branch with the name in &lt;code&gt;$BRANCH&lt;/code&gt; via &lt;code&gt;git checkout -b $BRANCH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, after all this is completed, a standard set of git commands follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;git-add -A .&lt;/code&gt; to stage all changes in the working directory,&lt;/li&gt;
&lt;li&gt;Configure a username and email address for who's doing the commit with &lt;code&gt;git config user.name&lt;/code&gt; and &lt;code&gt;git config user.email&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;Add a commit message with &lt;code&gt;git commit -am "Standard commit message here"&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;Lastly, push it to GitHub with &lt;code&gt;git push --set-upstream origin $BRANCH&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At this point, you should be good to go. The file should be successfully copied over to the other repository.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Inspiration for this second Bash script was initially based on this &lt;a href="https://remarkablemark.org/blog/2022/09/25/check-git-branch-exists-in-remote-repository/" rel="noopener noreferrer"&gt;blog post&lt;/a&gt; by Remarkable Mark.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Check that the workflow runs
&lt;/h3&gt;

&lt;p&gt;The simplest way to test this new functionality is just to keep an eye out for changes in the main branch of the repo that would trigger the workflow to run and see if it works.&lt;/p&gt;

&lt;p&gt;When you name the workflow a descriptive name, it's easy to find it amongst the various GitHub Action workflows the repo may have.&lt;/p&gt;

&lt;p&gt;Here's a screenshot of the workflow runs of the &lt;code&gt;Copy updated OpenAPI file&lt;/code&gt; job inside the GitHub repo's &lt;strong&gt;Actions&lt;/strong&gt; page.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/485b1572d0292816fc45e4be360de478/e9d87/gh-actions-workflow.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F485b1572d0292816fc45e4be360de478%2F1e043%2Fgh-actions-workflow.png" title="Multiple runs of a particular workflow in the GitHub Actions page of a repository" alt="Multiple runs of a particular workflow in the GitHub Actions page of a repository" width="690" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you want to dig in further to any of the runs to check the logs and job steps (especially handy for debugging purposes), just click on a workflow run, then the name of the job, and inside the job you can expand out each step.&lt;/p&gt;

&lt;p&gt;Here's another screenshot of the &lt;code&gt;Push to notehub-js repo&lt;/code&gt; step expanded to see exactly what the Bash script in that step is logging to the console via &lt;code&gt;echo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/d1cc8912c1d8339500c3d985ad375a14/6edca/gh-actions-job-details.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2Fd1cc8912c1d8339500c3d985ad375a14%2F1e043%2Fgh-actions-job-details.png" title="Details of a particular step inside of a GitHub Actions job expanded for review" alt="Details of a particular step inside of a GitHub Actions job expanded for review" width="690" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And you're done! The workflow's running, the steps are successfully completing, and, most importantly, the file from the Notehub repo is successfully copied to the Notehub JS repo. Mission accomplished.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;My first foray into building an open source software library on behalf of one of my company's APIs was a unique challenge that taught me a lot. Especially as I got to leverage GitHub Action workflows to automate as many parts of the process as I could to make ongoing maintenance and upkeep easier.&lt;/p&gt;

&lt;p&gt;One such task that I automated involved copying a file from one repo into another whenever a change was made to that particular file. And while I was able to leverage a few pre-existing GitHub Actions, I also had to write a couple custom Bash scripts for some of the trickier, more-unique-to-my-use-case steps.&lt;/p&gt;

&lt;p&gt;Fortunately, GitHub Action workflows allows for both types of steps to be combined in a workflow, making for some very powerful, turnkey solutions that did just what I needed.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you’d like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com" rel="noopener noreferrer"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope learning to copy a file from one repo to another automatically with GitHub Actions workflows is as helpful for you as it has been for me. Enjoy!&lt;/p&gt;




&lt;h2&gt;
  
  
  References &amp;amp; Further Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous OSS article &lt;a href="//./use-git-hub-actions-to-automatically-publish-a-repo-subfolder-as-an-npm-library"&gt;using GH Actions to publish a package to npm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Previous OSS article &lt;a href="//./automatically-create-a-pull-request-against-a-feature-branch-with-git-hub-actions"&gt;using GH Actions to create a new PR branch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/blues/notehub-js" rel="noopener noreferrer"&gt;Notehub JS&lt;/a&gt; GitHub repo&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>git</category>
      <category>bash</category>
      <category>devops</category>
    </item>
    <item>
      <title>Automatically Create a Pull Request Against a Feature Branch with GitHub Actions</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Sun, 11 Jun 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/paigen11/automatically-create-a-pull-request-against-a-feature-branch-with-github-actions-2pg7</link>
      <guid>https://dev.to/paigen11/automatically-create-a-pull-request-against-a-feature-branch-with-github-actions-2pg7</guid>
      <description>&lt;p&gt;&lt;a href="/static/209618d5209ec7dd5a2720fdbd1a80dd/0f98f/typewriter-hero.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F209618d5209ec7dd5a2720fdbd1a80dd%2F15ec7%2Ftypewriter-hero.jpg" title="Old school typewriter" alt="Old school typewriter" width="690" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This blog post is the second in a &lt;a href="https://www.paigeniedringhaus.com/blog/use-git-hub-actions-to-automatically-publish-a-repo-subfolder-as-an-npm-library" rel="noopener noreferrer"&gt;short series&lt;/a&gt; I'm writing about the many things I learned in the course of building my first open source API library for the IoT startup I work for, &lt;a href="https://blues.io/?&amp;amp;utm_source=paigeniedringhaus.com&amp;amp;utm_medium=web&amp;amp;utm_campaign=nf&amp;amp;utm_content=notehub-js" rel="noopener noreferrer"&gt;Blues&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It was a great learning experience for me and a new, unique challenge because I needed to do the following things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make a copy of the &lt;code&gt;openapi.yaml&lt;/code&gt; file from the Blues cloud's repository, &lt;a href="https://notehub.io/?&amp;amp;utm_source=dev.to&amp;amp;utm_medium=web&amp;amp;utm_campaign=nf&amp;amp;utm_content=notehub-js"&gt;Notehub&lt;/a&gt;, whenever the file was updated.&lt;/li&gt;
&lt;li&gt;Open a new pull request against the &lt;a href="https://github.com/blues/notehub-js" rel="noopener noreferrer"&gt;Notehub JS repo&lt;/a&gt; with the copy of the &lt;code&gt;openapi.yaml&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Generate a fresh version of the Notehub JS API library based on the that file via the &lt;a href="https://openapi-generator.tech/docs/installation" rel="noopener noreferrer"&gt;OpenAPI Generator CLI&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;And publish the updated Notehub JS library to &lt;a href="https://www.npmjs.com/package/@blues-inc/notehub-js" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And I wanted to automate as many of these steps as possible through the use of GitHub Actions workflows.&lt;/p&gt;

&lt;p&gt;Since I didn't know how often the Notehub's &lt;code&gt;openapi.yaml&lt;/code&gt; file would be updated, I needed a way to notify myself when a new version of the Notehub's API file needed review in the Notehub JS repository. The best solution I could think of was to open a new pull request in the Notehub JS repo after copying the updated &lt;code&gt;openapi.yaml&lt;/code&gt; file into a feature branch and tagging myself to review it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today, I'll walk through how to use a GitHub Actions workflow to create (or update) a pull request whenever a new feature branch is made in that repository.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Notehub JS
&lt;/h2&gt;

&lt;p&gt;Before we get to the actual GitHub Actions workflow, let me give you just a little background on the &lt;a href="https://github.com/blues/notehub-js" rel="noopener noreferrer"&gt;&lt;strong&gt;Notehub JS&lt;/strong&gt;&lt;/a&gt; project because it's a bit different than most.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This section helps explain the folder structure for the repo in case you want to explore it in GitHub, if you just want the solutions, feel free to jump down to the next section.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notehub JS is a JavaScript-based library for interacting with the native &lt;a href="https://dev.blues.io/reference/notehub-api/api-introduction/?&amp;amp;utm_source=paigeniedringhaus.com&amp;amp;utm_medium=web&amp;amp;utm_campaign=nf&amp;amp;utm_content=notehub-js" rel="noopener noreferrer"&gt;&lt;strong&gt;Notehub API&lt;/strong&gt;&lt;/a&gt;, and it's generated from the Notehub project's own &lt;code&gt;openapi.yaml&lt;/code&gt; file, which follows the &lt;a href="https://swagger.io/specification/" rel="noopener noreferrer"&gt;OpenAPI specification&lt;/a&gt; standards.&lt;/p&gt;

&lt;p&gt;The &lt;a href="(https://openapi-generator.tech/docs/installation)"&gt;OpenAPI Generator CLI&lt;/a&gt; is a tool that can use the &lt;code&gt;openapi.yaml&lt;/code&gt; file to create an entire library complete with documentation, models, endpoints, and scripts to package it up for publishing as an npm module. The end library that I care about publishing to npm is a &lt;em&gt;subfolder&lt;/em&gt; inside of the main Notehub JS repo. At the root of the project are the &lt;code&gt;openapi.yaml&lt;/code&gt; file, the GitHub Actions workflows, and a few other config files.&lt;/p&gt;

&lt;p&gt;Here's a simplified view of the &lt;a href="https://github.com/blues/notehub-js" rel="noopener noreferrer"&gt;Notehub JS repo&lt;/a&gt;'s folder structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root/
├── .github/ 
| ├── workflows/
| | ├── create-pr.yml 
| ├── PULL_REQUEST_TEMPLATE.md
├── src/ &amp;lt;- this is the folder generated by the OpenAPI Generator CLI
| ├── dist/
| ├── docs/
| ├── src/ 
| | ├── api/ 
| | ├── model/
| | ├── index.js 
| openapi.yaml
| config.json
| package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see in the diagram above, the &lt;code&gt;openapi.yaml&lt;/code&gt; file that this library is generated from lives at the root of the repo, and the &lt;code&gt;src/&lt;/code&gt; folder is what actually holds all the Notehub API JavaScript code that powers the &lt;a href="https://www.npmjs.com/package/@blues-inc/notehub-js" rel="noopener noreferrer"&gt;Notehub JS library&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;openapi.yaml&lt;/code&gt; file is what gets copied from the Notehub repo when changes are made to it. And changes are made to the file whenever the Notehub API is updated with new features and functionality, so making sure that the Notehub JS library keeps up with the new additions to the API it's designed to interact with is important.&lt;/p&gt;

&lt;p&gt;Now that I've explained a bit more about the Notehub JS repo and why keeping it in sync with the Notehub API is important, we can get down to the business of automating PRs for this repo.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Looking for more details about Notehub JS?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check out my &lt;a href="https://www.paigeniedringhaus.com/blog/use-git-hub-actions-to-automatically-publish-a-repo-subfolder-as-an-npm-library" rel="noopener noreferrer"&gt;previous blog post&lt;/a&gt; about how to use GitHub Actions to automatically publish new releases to npm - I do a fairly deep dive on Notehub JS there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create a standard PR template for the repo
&lt;/h3&gt;

&lt;p&gt;When I was just a few years into my own web development career, I was introduced to &lt;a href="https://betterprogramming.pub/github-templates-the-smarter-way-to-formalize-pull-requests-among-development-teams-89f8d6a204f" rel="noopener noreferrer"&gt;GitHub PR templates&lt;/a&gt;, an easier way to keep pull requests uniform for a repository worked on by multiple developers. It made the task of creating decent PRs so much better.&lt;/p&gt;

&lt;p&gt;There is not a team or repo I join now without adding a &lt;code&gt;./github/&lt;/code&gt; directory folder and &lt;code&gt;PULL_REQUEST_TEMPLATE.md&lt;/code&gt; file if it doesn't already exist.&lt;/p&gt;

&lt;p&gt;For the Notehub JS repo, as it's mostly autogenerated, I included a couple of sections to fill in: &lt;code&gt;Problem Context&lt;/code&gt;: a brief description of the updates in the PR, and &lt;code&gt;Changes&lt;/code&gt;: what code changes were actually made in the PR. Simple enough for any dev to fill out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/blues/notehub-js/blob/main/.github/PULL_REQUEST_TEMPLATE.md" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;PULL_REQUEST_TEMPLATE.md&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Problem Context&lt;/span&gt;

&lt;span class="gu"&gt;## Changes&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That's all I wanted to add to the repo before focusing on the GitHub Actions workflow itself. We'll get to it next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up a GitHub Actions workflow to automatically create pull requests
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Need a refresher on GitHub Actions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want a quick primer on what GitHub Actions are, I recommend you check out a previous article I wrote about them &lt;a href="https://www.paigeniedringhaus.com/blog/use-secret-environment-variables-in-git-hub-actions#what-is-github-actions" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For this particular workflow, I was able to string together a few premade &lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; to do just what I needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new pull request whenever a new feature branch was pushed to the GitHub repository.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;PULL_REQUEST_TEMPLATE.md&lt;/code&gt; file to format the PR.&lt;/li&gt;
&lt;li&gt;Notify me when a PR is there for review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what the final &lt;code&gt;create-pr.yml&lt;/code&gt; file looks like inside of the &lt;code&gt;./github/workflows/&lt;/code&gt; directory. I'll dissect it below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/blues/notehub-js/blob/main/.github/workflows/create-pr.yml" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;create-pr.yml&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Automatically create / update pull request&lt;/span&gt;

&lt;span class="c1"&gt;# run this workflow only on new feature branches, not when they're merged to main&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches-ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;create_pr_repo_sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create pull request&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;open-pr&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;repo-sync/pull-request@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;destination_branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt;
          &lt;span class="na"&gt;pr_title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feat:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PLACEHOLDER&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;TITLE"&lt;/span&gt;
          &lt;span class="na"&gt;pr_template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.github/PULL_REQUEST_TEMPLATE.md"&lt;/span&gt;
          &lt;span class="na"&gt;pr_reviewer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paigen11"&lt;/span&gt;
          &lt;span class="na"&gt;pr_draft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each workflow file needs a &lt;strong&gt;&lt;code&gt;name&lt;/code&gt;&lt;/strong&gt; , so I chose to name this one: &lt;code&gt;Automatically create / update pull request&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This workflow should be triggered whenever a new branch is pushed to the repo &lt;em&gt;but&lt;/em&gt; it should ignore the &lt;code&gt;"main"&lt;/code&gt; branch. &lt;code&gt;"main"&lt;/code&gt; is the main branch of this repo that gets published to npm and doesn't need a PR to be created for it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches-ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;on&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is how a workflow is triggered.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpushbranchestagsbranches-ignoretags-ignore" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;push&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is the event that triggers the workflow.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-excluding-branches-and-tags" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;branches-ignore&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is how to exclude the workflow from running on certain branch patterns. This gives us more fine-grained control of when the workflow should run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the &lt;strong&gt;&lt;code&gt;jobs&lt;/code&gt;&lt;/strong&gt; section runs inside of the workflow. This particular script only has one job, &lt;code&gt;create_pr_repo_sync&lt;/code&gt;, but if there's multiple jobs, they'll run sequentially unless otherwise specified.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;create_pr_repo_sync&lt;/code&gt; job defines that it runs on the latest version of Ubuntu in &lt;strong&gt;&lt;code&gt;runs-on&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Finally, I get to the &lt;strong&gt;&lt;code&gt;steps&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The steps are as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checkout the code so the workflow can access it using the GitHub Action &lt;a href="https://github.com/actions/checkout" rel="noopener noreferrer"&gt;&lt;code&gt;actions/checkout@v3&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Create a pull request using the GitHub Action &lt;a href="https://github.com/repo-sync/pull-request" rel="noopener noreferrer"&gt;&lt;code&gt;repo-sync/pull-request@v2&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pass the PR action a custom &lt;code&gt;pr_title&lt;/code&gt;, &lt;code&gt;pr_template&lt;/code&gt;, and &lt;code&gt;pr_reviewer&lt;/code&gt;. Set it to be created as a &lt;code&gt;pr_draft&lt;/code&gt; (I like PRs to be drafts until I've looked them over and know they're ready for review), and open the PR branch against the &lt;code&gt;destination_branch: "main"&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the &lt;code&gt;pr_template&lt;/code&gt; section, I pass the path to the PR template file I made in the previous section, and for the &lt;code&gt;pr_reviewer&lt;/code&gt; I add my own GitHub username so that I get an email notification when the new PR is created in GitHub.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test out the workflow
&lt;/h3&gt;

&lt;p&gt;If you want to test this functionality out, create a new local branch of the repo, make a change in the branch, and push it to GitHub. When that new branch registers, the GitHub Actions workflow will be triggered to run.&lt;/p&gt;

&lt;p&gt;If you visit the &lt;strong&gt;Actions&lt;/strong&gt; page in the GitHub repo, you should see the "Automatically create / update pull request" job running.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/9e62484e7aa49007e3d6008d55472a64/20766/gh-action-workflow.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2F9e62484e7aa49007e3d6008d55472a64%2F1e043%2Fgh-action-workflow.png" title="GitHub Actions workflow running in Notehub JS repo's Actions tab" alt="GitHub Actions workflow running in Notehub JS repo's Actions tab" width="690" height="103"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And after the action finishes running, there should be an email sent to the folks tagged as the PR reviewers in the GitHub Action (me, in this case).&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/fe8357b2a004d3efa65400b7ed177268/2a333/gh_action_email.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paigeniedringhaus.com%2Fstatic%2Ffe8357b2a004d3efa65400b7ed177268%2F1e043%2Fgh_action_email.png" title="Notification email from GitHub to review the newly created PR" alt="Notification email from GitHub to review the newly created PR" width="690" height="648"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And with that, the workflow working has been confirmed, and the reviewers know it's time to take action in that repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When I built my first open source software library, I learned a lot. My software is a JavaScript library based on my company's API, Notehub, so every time the Notehub API gets updated, the JS library based off of it, needs to be updated too.&lt;/p&gt;

&lt;p&gt;I needed a programmatic way to let myself know whenever new changes were made to the JS repo that needed to be reviewed.&lt;/p&gt;

&lt;p&gt;The easiest way I could think to accomplish this was by automating the pull request creation so that when a new feature branch was pushed to the repo, it opened a PR and tagged myself as the reviewer.&lt;/p&gt;

&lt;p&gt;Luckily, a GitHub Action existed for just such a scenario, allowing for designating a PR template, a PR reviewer, giving the PR a placeholder title, and a whole host of other configurations. The workflow made it quite straightforward, and gave me the peace of mind that whenever changes are made that I need to review, I get an email to look at what's changed.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more about the useful things I learned while building this project in addition to other topics on JavaScript, React, IoT, or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you’d like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com" rel="noopener noreferrer"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope learning to use GitHub Actions workflows to automate a new PR when a feature branch is pushed to a repository proves useful. Enjoy!&lt;/p&gt;




&lt;h2&gt;
  
  
  References &amp;amp; Further Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous OSS article &lt;a href="https://www.paigeniedringhaus.com/blog/use-git-hub-actions-to-automatically-publish-a-repo-subfolder-as-an-npm-library" rel="noopener noreferrer"&gt;using GH Actions to publish a package to npm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/blues/notehub-js" rel="noopener noreferrer"&gt;Notehub JS&lt;/a&gt; GitHub repo&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository" rel="noopener noreferrer"&gt;GitHub PR templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/repo-sync/pull-request" rel="noopener noreferrer"&gt;Pull request&lt;/a&gt; GitHub Action&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>git</category>
      <category>npm</category>
      <category>devops</category>
    </item>
    <item>
      <title>Use GitHub Actions to Automatically Publish a Repo Subfolder as an npm Library</title>
      <dc:creator>Paige Niedringhaus</dc:creator>
      <pubDate>Wed, 12 Apr 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/paigen11/use-github-actions-to-automatically-publish-a-repo-subfolder-as-an-npm-library-13a</link>
      <guid>https://dev.to/paigen11/use-github-actions-to-automatically-publish-a-repo-subfolder-as-an-npm-library-13a</guid>
      <description>&lt;p&gt;&lt;a href="///static/213d0cc6af1f5b11dd550cd7d9e30faa/a2510/workflow-hero.jpg"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IKQyItHv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.paigeniedringhaus.com/static/213d0cc6af1f5b11dd550cd7d9e30faa/15ec7/workflow-hero.jpg" alt="Person diagramming out a website workflow" title="Person diagramming out a website workflow" width="690" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I've said it before, and I'll say it again, working as a software engineer for a startup is fun and challenging because of the sheer number of different things I get to do on a regular basis.&lt;/p&gt;

&lt;p&gt;I work for an Internet of Things startup called &lt;a href="https://blues.io/?&amp;amp;utm_source=dev.to&amp;amp;utm_medium=web&amp;amp;utm_campaign=nf&amp;amp;utm_content=notehub-js"&gt;Blues&lt;/a&gt; which specializes in getting IoT data from devices in the real world into the cloud via cellular. To help show our customers all the possibilities of what they can do with our hardware (Notecards) and our cloud (Notehub), a group of my coworkers and I have been building &lt;a href="https://dev.blues.io/accelerators/?&amp;amp;utm_source=dev.to&amp;amp;utm_medium=web&amp;amp;utm_campaign=nf&amp;amp;utm_content=notehub-js"&gt;lots of JavaScript-based web apps&lt;/a&gt; to monitor and display IoT data for a variety of different use cases.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A little more about Notecards and Notehub.io&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blues.io/products/notecard/?&amp;amp;utm_source=dev.to&amp;amp;utm_medium=web&amp;amp;utm_campaign=nf&amp;amp;utm_content=notehub-js"&gt;&lt;strong&gt;Notecards&lt;/strong&gt;&lt;/a&gt; are low-power, 30mm x 35mm, prepaid cellular-connected devices designed to integrate with any IoT device, and Notecards know, out of the box, how to connect with the Notehub cloud to send as well as receive data from it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://notehub.io/?&amp;amp;utm_source=dev.to&amp;amp;utm_medium=web&amp;amp;utm_campaign=nf&amp;amp;utm_content=notehub-js"&gt;&lt;strong&gt;Notehub.io&lt;/strong&gt;&lt;/a&gt; is a cloud-based service for securely connecting to Notecard devices and storing data from them before sending it on to your cloud application of choice.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It quickly became apparent that rewriting the same HTTP calls in multiple projects to fetch data from the native Notehub API endpoints wasn’t easy to reuse or keep up to date as the API continues to grow and evolve.&lt;/p&gt;

&lt;p&gt;So we created our own open source, &lt;a href="https://github.com/blues/notehub-js"&gt;JavaScript-based library&lt;/a&gt; for the Notehub API and published it on &lt;a href="https://www.npmjs.com/package/@blues-inc/notehub-js"&gt;npm&lt;/a&gt; for anyone to use for free. Notehub JS is designed to get you connected to the Notehub API quickly, and allow you to access all of the API routes relevant to interacting with Notehub in a JavaScript-friendly way.&lt;/p&gt;

&lt;p&gt;Figuring out how to build an open source library like this and publish it to npm was a first for me, and I learned a lot of interesting things along the way that I'd like to share with you all over the course of a few blog posts in the hope that it might help you if you decide to undertake a similar effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In this blog, I'll show you how to set up a GitHub Actions workflow to automatically publish a new release of a GitHub repo to npm - no muss, no fuss, no manual input required.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Notehub JS
&lt;/h2&gt;

&lt;p&gt;Ok, before we get to the GitHub Actions workflow, I want to give you a little background on the Notehub JS project because it's a bit unusual.&lt;/p&gt;

&lt;p&gt;As I mentioned above, Notehub JS is a JavaScript-based library for interacting with the native &lt;a href="https://dev.blues.io/reference/notehub-api/api-introduction/?&amp;amp;utm_source=dev.to&amp;amp;utm_medium=web&amp;amp;utm_campaign=nf&amp;amp;utm_content=notehub-js"&gt;Notehub API&lt;/a&gt;, and it's actually generated from the Notehub project's own &lt;code&gt;openapi.yaml&lt;/code&gt; file, which follows the &lt;a href="https://swagger.io/specification/"&gt;OpenAPI specification&lt;/a&gt; standards.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;OpenAPI Specification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to HTTP APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service with a minimal amount of implementation logic.&lt;/p&gt;

&lt;p&gt;An OpenAPI definition can then be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  OpenAPI Generator CLI
&lt;/h3&gt;

&lt;p&gt;Because the Notehub API has an &lt;code&gt;openapi.yaml&lt;/code&gt; file that is updated regularly, I was able to use the &lt;a href="https://openapi-generator.tech/docs/installation"&gt;&lt;strong&gt;OpenAPI Generator CLI&lt;/strong&gt;&lt;/a&gt; to automatically generate a JavaScript-based library to interact with the Notehub API in just a few commands from a terminal.&lt;/p&gt;

&lt;p&gt;The catch is, when the new folder is generated which has all the API endpoints and models, the documents, and the &lt;code&gt;dist/&lt;/code&gt; folder that packages everything up for consumption as an npm module, it's stored as a &lt;em&gt;subfolder&lt;/em&gt; inside of the main Notehub JS repo.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;openapi.yaml&lt;/code&gt; file lives at the root level of the project along with its &lt;code&gt;config.json&lt;/code&gt; file, a &lt;code&gt;package.json&lt;/code&gt; file, and a few other other bits and bobs (license, contribution guidelines, code of conduct, etc.), but the real meat of the library lives inside of the &lt;code&gt;src/&lt;/code&gt; subfolder.&lt;/p&gt;

&lt;p&gt;Here's a simplified view of the &lt;a href="https://github.com/blues/notehub-js"&gt;Notehub JS repo&lt;/a&gt;'s folder structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root/
├── .github/ 
| ├── workflows/
| | ├── publish-npm.yml 
├── src/ &amp;lt;- this is the folder generated by the OpenAPI Generator CLI
| ├── dist/
| ├── docs/
| ├── src/ 
| | ├── api/ 
| | ├── model/
| | ├── index.js 
| openapi.yaml
| config.json
| package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the diagram above you can see the &lt;code&gt;openapi.yaml&lt;/code&gt; file that this library is generated from lives at the root of the repo, and the &lt;code&gt;src/&lt;/code&gt; folder is what actually holds all the Notehub API JavaScript code that powers the Notehub JS library.&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;src/&lt;/code&gt; folder is what needs to be published to npm - not the root of the repo as is the case with most projects in GitHub. But not to worry, publishing just this subfolder can be done, and better yet, it can be automated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure the root &lt;code&gt;package.json&lt;/code&gt; to point to the subfolder where the Notehub JS library code lives
&lt;/h3&gt;

&lt;p&gt;So now that I've covered &lt;em&gt;why&lt;/em&gt; I only need to publish the subfolder of the Notehub JS repo to npm, let's get on to the &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;First thing: add a few extra configuration details to the &lt;a href="https://github.com/blues/notehub-js/blob/main/package.json"&gt;&lt;code&gt;package.json&lt;/code&gt; at the root of the project&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update the &lt;code&gt;main&lt;/code&gt; field to point to the subfolder's &lt;code&gt;index.js&lt;/code&gt; file&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;According to the npm docs, the &lt;a href="https://docs.npmjs.com/cli/v9/configuring-npm/package-json#main"&gt;&lt;code&gt;main&lt;/code&gt;&lt;/a&gt; field points to the primary entry point of an app, and when it's not specifically defined it defaults to &lt;code&gt;index.js&lt;/code&gt; at the root of the project.&lt;/p&gt;

&lt;p&gt;Since the &lt;code&gt;src/dist/&lt;/code&gt; folder is where the &lt;code&gt;index.js&lt;/code&gt; file lives for Notehub JS library code that should be uploaded to npm, &lt;code&gt;main&lt;/code&gt; needs to be updated to point to it instead.&lt;/p&gt;

&lt;p&gt;Now the &lt;code&gt;package.json&lt;/code&gt;'s &lt;code&gt;main&lt;/code&gt; field should read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/dist/index.js"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Update &lt;code&gt;repository&lt;/code&gt; to include a &lt;code&gt;directory&lt;/code&gt; field&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next, the &lt;a href="https://docs.npmjs.com/cli/v9/configuring-npm/package-json#repository"&gt;&lt;code&gt;repository&lt;/code&gt;&lt;/a&gt; field in a &lt;code&gt;package.json&lt;/code&gt; specifies where the code lives, and it's generally helpful for people who want to contribute to the project.&lt;/p&gt;

&lt;p&gt;It has another benefit though: if the &lt;code&gt;package.json&lt;/code&gt; for the npm package is not in the root directory (like for us, where the auto-generated &lt;code&gt;src/&lt;/code&gt; folder has its own &lt;code&gt;package.json&lt;/code&gt;), you can specify the directory in which that file lives.&lt;/p&gt;

&lt;p&gt;Here's what the &lt;code&gt;repository&lt;/code&gt; field should look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/blues/notehub-js.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"directory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this translates to on the npm package page is a handy link that points back to root of the GitHub folder (see image below).&lt;/p&gt;

&lt;p&gt;&lt;a href="///static/00e3a061fba537fd942151fd22bb311a/a7396/npm-repo-link.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RRgzeC91--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.paigeniedringhaus.com/static/00e3a061fba537fd942151fd22bb311a/1e043/npm-repo-link.png" alt="Notehub JS npm page highlighting repository link on the page" title="Notehub JS npm page highlighting repository link on the page" width="690" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add the &lt;code&gt;publishConfig&lt;/code&gt; field to specify &lt;code&gt;directory&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Typically, the &lt;a href="https://docs.npmjs.com/cli/v9/configuring-npm/package-json#publishconfig"&gt;&lt;code&gt;publishConfig&lt;/code&gt;&lt;/a&gt; field is used to set config values that will be used at publish time like tags, registries, or access.&lt;/p&gt;

&lt;p&gt;Another field that can be added here is &lt;code&gt;directory&lt;/code&gt;, which customizes the published subdirectory relative to the current &lt;code&gt;package.json&lt;/code&gt;. This ensures npm knows the &lt;code&gt;package.json&lt;/code&gt; it needs to read from (and display) in the npm package page is inside of the auto-generated &lt;code&gt;src/&lt;/code&gt; folder, not at the root of the project.&lt;/p&gt;

&lt;p&gt;Add the following &lt;code&gt;publishConfig&lt;/code&gt; code to the &lt;code&gt;package.json&lt;/code&gt; like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"publishConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"registry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://registry.npmjs.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"directory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the end, the whole &lt;code&gt;package.json&lt;/code&gt; at the root of the Notehub JS repo looks like the code snippet below.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I've omitted parts of the &lt;code&gt;package.json&lt;/code&gt; for clarity, but if you'd like to see the whole file, you can click the file name below. It's linked to the actual file in the repo in GitHub.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/blues/notehub-js/blob/main/package.json"&gt;&lt;strong&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notehub-js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JavaScript library for accessing the Blues Wireless Notehub API endpoints"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/blues/notehub-js.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"directory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publishConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"registry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://registry.npmjs.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"directory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great, the changes to the &lt;code&gt;package.json&lt;/code&gt; to publish just the &lt;code&gt;src/&lt;/code&gt; folder to npm are complete, let's move on to the GitHub Actions workflow to make publishing a new version of the Notehub JS library to npm as automated as possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a GitHub Actions workflow to publish to npm
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Need a refresher on GitHub Actions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want a quick primer on what GitHub Actions are, I recommend you check out a previous article I wrote about them &lt;a href="https://www.paigeniedringhaus.com/blog/use-secret-environment-variables-in-git-hub-actions#what-is-github-actions"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I decided that for my use case, creating a new &lt;a href="https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases"&gt;&lt;strong&gt;release&lt;/strong&gt;&lt;/a&gt; made the most sense to trigger a GitHub Actions workflow to publish the Notehub JS &lt;code&gt;src/&lt;/code&gt; subfolder to npm.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;GitHub specifically defines a &lt;strong&gt;release&lt;/strong&gt; as a deployable software iteration that you can package and make available for a wider audience to download and use, which is exactly what I want.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once I had settled on this as the trigger for my GitHub Actions workflow, it was a pretty straightforward set of steps to publish to npm.&lt;/p&gt;

&lt;p&gt;Here's what the finished &lt;code&gt;publish-npm.yml&lt;/code&gt; file will look like inside of the &lt;code&gt;./github/workflows/&lt;/code&gt; folder - I'll break it all down afterwards.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/blues/notehub-js/blob/main/.github/workflows/publish-npm.yml"&gt;&lt;strong&gt;&lt;code&gt;publish-npm.yml&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish notehub-js to npm&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;created&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;npm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup .npmrc file to publish to npm&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;16.x"&lt;/span&gt;
          &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://registry.npmjs.org"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish to npm&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm publish&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;NODE_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NPM_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each workflow file needs a &lt;strong&gt;&lt;code&gt;name&lt;/code&gt;&lt;/strong&gt;, so I chose a descriptive one: &lt;code&gt;Publish notehub-js to npm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, as I already explained above, this workflow is triggered whenever a new release is created in GitHub, which is where&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;created&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;comes into play.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on"&gt;&lt;strong&gt;&lt;code&gt;on&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is how a workflow is triggered.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release"&gt;&lt;strong&gt;&lt;code&gt;release&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is the event that triggers the workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;types: [created]&lt;/code&gt;&lt;/strong&gt; is the activity type for a &lt;code&gt;release&lt;/code&gt; event that triggers the workflow. This gives us more fine-grained control of when the workflow should run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the &lt;strong&gt;&lt;code&gt;jobs&lt;/code&gt;&lt;/strong&gt; section runs inside of the workflow. This particular script only has one job, &lt;code&gt;npm&lt;/code&gt;, but if there's multiple jobs, they'll run sequentially unless otherwise specified.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;npm&lt;/code&gt; job defines that it runs on the latest version of Ubuntu in &lt;strong&gt;&lt;code&gt;runs-on&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defaultsrun"&gt;&lt;strong&gt;&lt;code&gt;defaults.run&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is an important part of the script because it's where I specify that the working directory for all the following steps is the &lt;code&gt;src/&lt;/code&gt; folder (because I don't need any of the files from the root of the repo, just what's inside the &lt;code&gt;src/&lt;/code&gt; folder, I can define this at the job level).&lt;/p&gt;

&lt;p&gt;Finally, I get to the &lt;strong&gt;&lt;code&gt;steps&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The steps are as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checkout the code so the workflow can access it using the GitHub Action &lt;a href="https://github.com/actions/checkout"&gt;&lt;code&gt;actions/checkout@v3&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Download a Node.js version to build the package before publishing it to npm using the GitHub Action &lt;a href="https://github.com/actions/setup-node"&gt;&lt;code&gt;actions/setup-node@v3&lt;/code&gt;&lt;/a&gt; (specify the &lt;code&gt;node-version&lt;/code&gt; and &lt;code&gt;registry-url&lt;/code&gt; for npm here).&lt;/li&gt;
&lt;li&gt;Install the project's dependencies stored in the &lt;code&gt;src/&lt;/code&gt; folder's &lt;code&gt;package.json&lt;/code&gt; file using &lt;a href="https://github.com/actions/setup-node"&gt;&lt;code&gt;npm ci&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Publish to npm by running the command &lt;a href="https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry"&gt;&lt;code&gt;npm publish&lt;/code&gt;&lt;/a&gt; and supplying it with an &lt;a href="https://docs.npmjs.com/creating-and-viewing-access-tokens"&gt;&lt;code&gt;NPM_TOKEN&lt;/code&gt;&lt;/a&gt; (generated in npm) to verify this workflow has permissions to publish to the specified npm package registry.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And there you have it: each time a new release is created in the Notehub JS repo, this GitHub Actions workflow will run and deploy the updated code to npm.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Working as an engineer for a startup company is cool because no two days are alike for me. One day I'm building web apps and updating documentation on our developer experience site, the next I'm reading up on best practices for npm packages and how to automate tasks with GitHub Actions.&lt;/p&gt;

&lt;p&gt;Venturing into the realm of open source software and publishing my first JavaScript package to npm was quite a learning experience for me, but I'm so grateful I had the chance to push myself into this space and create something useful for my team (and hopefully our users as well!).&lt;/p&gt;

&lt;p&gt;One thing I knew I wanted, right from the start, was to automate the tasks I knew I'd be repeating fairly regularly over time: namely, regenerating the JavaScript library as the Notehub API continued to evolve, and redeploying a new version of the library to npm with the latest updates.&lt;/p&gt;

&lt;p&gt;With some extra configurations in the Notehub JS repo's &lt;code&gt;package.json&lt;/code&gt;, I was able to focus in on the auto-generated subfolder that contains the useful JavaScript-based code for easily interacting with the Notehub API, and with a few GitHub Actions inside of a workflow, I was able to build the code for the package and publish it to npm with the click of a button. Not too bad for an end result.&lt;/p&gt;

&lt;p&gt;Check back in a few weeks — I’ll be writing more about the useful things I learned while building this project in addition to other topics on JavaScript, React, IoT, or something else related to web development.&lt;/p&gt;

&lt;p&gt;If you’d like to make sure you never miss an article I write, sign up for my newsletter here: &lt;a href="https://paigeniedringhaus.substack.com"&gt;https://paigeniedringhaus.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope learning how to target a specific folder inside of a project and automating deployments to npm through GitHub Actions workflows come in handy for you in the future. Enjoy!&lt;/p&gt;




&lt;h2&gt;
  
  
  References &amp;amp; Further Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/blues/notehub-js"&gt;Notehub JS&lt;/a&gt; GitHub repo&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@blues-inc/notehub-js"&gt;notehub-js&lt;/a&gt; npm library&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openapi-generator.tech/docs/installation"&gt;OpenAPI Generator CLI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>github</category>
      <category>devops</category>
      <category>npm</category>
    </item>
  </channel>
</rss>
