<?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: peachstone</title>
    <description>The latest articles on DEV Community by peachstone (@peachstone).</description>
    <link>https://dev.to/peachstone</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023801%2F7aeae72c-caa9-4b51-bac7-3f04cdf206ed.png</url>
      <title>DEV Community: peachstone</title>
      <link>https://dev.to/peachstone</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/peachstone"/>
    <language>en</language>
    <item>
      <title>How I Built a Moon-Phase Tarot App with Zero External API Calls</title>
      <dc:creator>peachstone</dc:creator>
      <pubDate>Fri, 10 Jul 2026 10:30:20 +0000</pubDate>
      <link>https://dev.to/peachstone/how-i-built-a-moon-phase-tarot-app-with-zero-external-api-calls-4plh</link>
      <guid>https://dev.to/peachstone/how-i-built-a-moon-phase-tarot-app-with-zero-external-api-calls-4plh</guid>
      <description>&lt;h4&gt;
  
  
  The Problem: Most Tarot Apps Are Random
&lt;/h4&gt;

&lt;p&gt;When I started building &lt;a href="https://www.lunatarotapp.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Luna Tarot&lt;/strong&gt;&lt;/a&gt; — a web app that gives you a daily tarot reading tied to the actual moon phase — I faced a design decision that would shape the entire project.&lt;/p&gt;

&lt;p&gt;Most tarot apps work like this: shuffle, deal, look up a card meaning from a table, display. The card you pull is random. The meaning never changes. There's no connection to anything external — no rhythm, no context.&lt;/p&gt;

&lt;p&gt;I wanted something different. I wanted each reading to be grounded in something real: &lt;a href="https://www.lunatarotapp.com/en/moon_phase_calendar" rel="noopener noreferrer"&gt;&lt;strong&gt;the actual phase of the moon&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So I needed a way to calculate the current moon phase — accurately, reliably, and without relying on third-party APIs.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Solution: Astronomy Engine
&lt;/h4&gt;

&lt;p&gt;I discovered** Astronomy Engine** — an open-source library for calculating positions of the Sun, Moon, and planets.&lt;/p&gt;

&lt;p&gt;It's available as an npm package and supports both browser and Node.js environments. Here's why it stood out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accurate&lt;/strong&gt;: Built on VSOP87 and NOVAS, validated against JPL Horizons data — precise to within &lt;strong&gt;±1 arcminute&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lightweight&lt;/strong&gt;: The minified bundle is only &lt;strong&gt;~120KB&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fast&lt;/strong&gt;: Moon-phase calculations run in under &lt;strong&gt;5ms&lt;/strong&gt; — zero impact on page load or UI responsiveness.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what it can do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Calculate the Moon's phase (expressed as an ecliptic longitude)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Find quarter moon phases (new, first quarter, full, third quarter)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Calculate rise, set, and culmination times&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Predict lunar and solar eclipses&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And much more&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why Zero API Calls?
&lt;/h4&gt;

&lt;p&gt;Most developers would reach for a weather or astronomy API. Here's why I didn't:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Privacy by default&lt;/strong&gt;: For a spiritual practice app, users shouldn't have to wonder if their reading data is being sent to some third-party service. By calculating everything locally, the app never makes any external requests — no API keys, no network calls, no tracking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No geolocation needed&lt;/strong&gt;: Moon phase and illumination percentage are global constants (unlike rise/set times, which depend on location). This means I never need to request the user's GPS data — eliminating another potential privacy leak at the source.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No costs, no outages&lt;/strong&gt;: No rate limits, no paywalls, no CORS proxy servers, and zero dependency on third-party uptime.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  How I Implemented It
&lt;/h4&gt;

&lt;p&gt;Here's the core TypeScript implementation I use in Luna Tarot:&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Astronomy&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astronomy-engine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Static metadata for each lunar phase&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;moonPhaseInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;new_moon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nameKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new-moon-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;descKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new-moon-desc-full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌑&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;waxing_crescent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nameKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waxing-crescent-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;descKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waxing-crescent-desc-full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌒&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;first_quarter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nameKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first-quarter-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;descKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first-quarter-desc-full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌓&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;waxing_gibbous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nameKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waxing-gibbous-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;descKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waxing-gibbous-desc-full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌔&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;full_moon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nameKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full-moon-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;descKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full-moon-desc-full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌕&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;waning_gibbous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nameKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waning-gibbous-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;descKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waning-gibbous-desc-full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌖&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;last_quarter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nameKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;last-quarter-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;descKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;last-quarter-desc-full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌗&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;waning_crescent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nameKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waning-crescent-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;descKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waning-crescent-desc-full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌘&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MoonPhaseType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;moonPhaseInfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Calculate precise lunar phases using astronomy-engine
 * phaseValue: 0-360 degrees, 0 = new moon, 180 = full moon
 * illumination: percentage of moon surface visible from Earth
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateMoonPhase&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="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MoonPhaseType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;illumination&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;phaseValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Astronomy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MoonPhase&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="c1"&gt;// Standard illumination formula: (1 - cos(phaseAngle)) / 2&lt;/span&gt;
  &lt;span class="c1"&gt;// 0° → 0%, 90° → 50%, 180° → 100%, 270° → 50%&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;illumination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phaseValue&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="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&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="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="c1"&gt;// Map 0-360° to 8 discrete phases (45° each)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MoonPhaseType&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new_moon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waxing_crescent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first_quarter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waxing_gibbous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full_moon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waning_gibbous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;last_quarter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waning_crescent&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;index&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;phaseValue&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;22.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;45&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;phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;phases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new_moon&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="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;illumination&lt;/span&gt;&lt;span class="p"&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;illumination&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="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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With just a few lines of code, I can determine exactly where the moon is in its cycle — and use that to change the tarot reading dynamically.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Alternative approach: If you prefer explicit range checks for readability, the classic &lt;code&gt;if/else if&lt;/code&gt; chain works perfectly too. The array-mapping version above is just a compact, functional alternative I ended up using.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  The Result
&lt;/h4&gt;

&lt;p&gt;Luna Tarot now delivers daily tarot readings that shift with the actual lunar cycle. The same card on a New Moon (beginnings, planting seeds) tells a different story than on a Full Moon (culmination, release).&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjr8jtd4zivxohdbpwao6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjr8jtd4zivxohdbpwao6.jpg" alt=" " width="799" height="382"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Caption: The main dashboard blends daily tarot guidance with the current lunar energy — all computed client-side.&lt;/em&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fny7n9k6dwp34tqswvwxa.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fny7n9k6dwp34tqswvwxa.jpg" alt=" " width="799" height="382"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Caption: The calendar shows real-time phase, illumination percentage, and traditional almanac data, updated instantly for any date.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The full tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Next.js 14.2 + React 18 + TypeScript — frontend framework&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tailwind CSS — styling&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;astronomy-engine — moon phase calculations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;lunar-javascript — Chinese lunar calendar conversion&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Python + NumPy DSP — procedurally generated meditation music (pre‑rendered as audio assets)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs in the browser. No API calls. No server dependencies for the core functionality.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🧑‍💻 Development Note: I used GLM-5.2 as an AI coding assistant (similar to Copilot) during development for translating content and generating boilerplate. However, the production app itself makes zero external API calls — no AI models, no third-party services, no tracking.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Key Takeaways
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;You don't always need an API. For many types of data (astronomy, math, calendars), local calculation is not only possible but often better — faster, cheaper, and more reliable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open-source libraries are powerful. Astronomy Engine is a hidden gem — accurate, well-tested, and available in multiple languages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Privacy is a feature. When you eliminate external calls, you eliminate data sharing concerns by default.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start with the user experience. I chose local calculation not because it was easier, but because it created a better, more private experience for users.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Try It Yourself
&lt;/h4&gt;

&lt;p&gt;Luna Tarot is live and completely free: &lt;a href="https://www.lunatarotapp.com/" rel="noopener noreferrer"&gt;https://www.lunatarotapp.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No account required. Just you, the cards, and the moon. 🌙&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're interested in the astronomy-engine library, check out the&lt;/em&gt; &lt;a href="https://github.com/cosinekitty/astronomy" rel="noopener noreferrer"&gt;&lt;em&gt;GitHub repository&lt;/em&gt;&lt;/a&gt; &lt;em&gt;or the&lt;/em&gt; &lt;a href="https://www.npmjs.com/package/astronomy-engine" rel="noopener noreferrer"&gt;&lt;em&gt;npm package&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;📌 Suggested Tags for &lt;a href="https://dev.to/"&gt;DEV.to&lt;/a&gt;:&lt;br&gt;
&lt;code&gt;nextjs&lt;/code&gt;, &lt;code&gt;typescript&lt;/code&gt;, &lt;code&gt;webdev&lt;/code&gt;, &lt;code&gt;javascript&lt;/code&gt;, &lt;code&gt;astronomy&lt;/code&gt;, &lt;code&gt;opensource&lt;/code&gt;, &lt;code&gt;tailwindcss&lt;/code&gt;, &lt;code&gt;privacy&lt;/code&gt;, &lt;code&gt;frontend&lt;/code&gt;, &lt;code&gt;indiedev&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;📌 Discussion Prompt (Pin this in comments):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Have you built calendar, weather, or astronomy tools that avoid third-party APIs? What math libraries or local-computation tricks did you use? I'd love to compare approaches in the comments!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>nextjs</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
