Portfolio Case Study
moondial
An e-ink celestial almanac for iOS and Android, built with React Native for moon phases, sun and moon timing, photo-light windows, Zmanim, and timezone-aware locations
Beta candidate · iOS and Android 1.1 (6)
Origin
moondial began as Astronomical Calendar, a native re-architecture of the web calendar at bachtogauss.com/calendar. The web version is something you sit down with. The app is something you reach for in the moment: outside at night, watching the moon, checking tomorrow's light, or wondering when the next visible event is coming.
Overview
moondial turns the original browser calendar into a Today-first mobile almanac. It keeps the real astronomy, eclipse, photography-light, and Zmanim calculations, but presents them through a quieter interface built for quick reading: a moon-led Today screen, a continuous Calendar view, and Settings for location and display preferences.
The current app is branded around a pencil-on-paper e-ink system: Cormorant Garamond for display text, DM Mono for labels and tabular values, fine linework, muted graphite color, and light-mode-only surfaces. The polish work is deliberately rendering and interaction work; the calculation engines are treated as locked behavior.
The app coordinates independent calculation domains for sun and moon rise/set events, moon phases, eclipses, blue and golden hour windows, and halachic Zmanim. Location, timezone, persisted settings, custom SVG iconography, app startup behavior, and platform-specific release details are handled behind a three-tab interface: Today, Calendar, and Settings.
Today
Today is the front door. It is locked to the live date and reads like a small celestial instrument: location and coordinates at the top, a large animated moon, moon age and illumination, moonrise and moonset, a sky arc for sun and moon positions, a sunrise / solar noon / sunset ledger, photo-light windows, and optional Zmanim.
- Animated moon hero driven by the real calculated phase through
MoonDisk - Replayable sun and moon sky arc rendered by
SkyArcon separate visible tracks - Light ledger for sunrise, solar noon, sunset, blue hour, and golden hour
- Moonrise, moonset, sunrise, solar noon, and sunset values animate with
TimeSettleText - Sequential value reveals coordinated through
FadeInOnReplay - Startup overlay hides hydration flicker until persisted settings and first Today data are ready
- Date block, moon hero, light arc, and time values support targeted animation replay
Feature Set
Today
- Current-date e-ink almanac with animated moon, sky arc, and light ledger
- Moon illumination percentage, moon age, phase name, moonrise, and moonset
- Sunrise, solar noon, sunset, blue hour, golden hour, and optional Zmanim
- Bottom-tab transitions and Today animations tuned around Reduce Motion and launch readiness
Calendar
- Interactive six-row month calendar with day, month, and year navigation
- Moon phase glyphs and solar/lunar eclipse glyphs rendered in calendar cells
- Inline timetable for the selected date below the month grid
- This Month section for principal phases and eclipse rows
- Upcoming board for New Moon, Full Moon, and eclipse events
Settings
- GPS-based location and manual city, postal code, or coordinate search
- OpenStreetMap Nominatim forward and reverse geocoding
- Offline
tz-lookupIANA timezone resolution for selected locations - Sunday or Monday week start, 12-hour or 24-hour time format, and feature toggles
- Moon phase, solar eclipse, lunar eclipse, photography light, Zmanim, and GRA/MGA controls
- AsyncStorage-backed persistence for location and display preferences
Architecture
The app is organized around screen-level composition, calculation services, small adapter layers, and a compact global store. The architecture keeps astronomical and halachic calculations independent, then merges partial results so a failure in one domain does not erase usable data from another.
src/ components/ E-ink UI, MoonDisk, SkyArc, ledgers, controls components/icons/ Bottom tab icons components/symbols/ Phase and eclipse SVG symbols context/ Startup readiness and launch animation navigation/ React Navigation bottom tabs screens/ Today, Calendar, and Settings screens services/ Calendar engine, location, geocoding, moon/time data store/ Zustand calendar, location, settings, persistence styles/ Shared theme, colors, typography, spacing types/ Shared TypeScript interfaces utils/ Astronomy, halachic, eclipse, moon, timezone adapters
Calculation pipeline
Calendar navigation is render-first: the visible month and selected date update immediately, while cached or cold data attaches only when its returned key still matches the current view. That keeps interaction responsive even when eclipse or upcoming-event searches are still filling in.
-
calendarDataEngineowns Calendar data orchestration with memory-only caches for month events, selected-day timetables, and upcoming data - Calendar prefetching prioritizes visible phase and eclipse markers before below-the-fold timetable and upcoming work
-
moonPhaseEventServiceis the singleSearchMoonQuartercaller, so phase times stay consistent across calendar markers, moon data, and upcoming data -
timeService.fetchAllTimes()fetches astronomy and halachic data in parallel and merges partial results -
astroDataAdapterbridges calculation output into the screen-readableDayDatashape used by Today, Calendar,LightLedger, andSkyArc
Key files and components
| TodayScreen.tsx | Main Today composition: live date, moon hero, sky arc, light ledger, Zmanim, replay orchestration |
| CalendarScreen.tsx | Continuous month calendar, selected-day timetable, this-month rows, upcoming board |
| SettingsScreen.tsx | Location, display, calendar marker, section, and Zmanim controls |
| calendarDataEngine.ts | Memory caches, promise dedupe, stale-key guards, and cooperative prefetch scheduling |
| moonPhaseEventService.ts | Canonical month phase lookup and session cache for Astronomy Engine quarter searches |
| timeService.ts | Parallel astronomy and Hebcal/Zmanim orchestration with fault-tolerant merging |
| astroDataAdapter.ts | Adapter from calculation services to screen-readable day data |
| SkyArc.tsx | Sun and moon arc visualization with rendering-only rise/set calibration |
| MoonDisk.tsx | SVG phased moon used by Today, Calendar, and timetable views |
| LightLedger.tsx | Sunrise, solar noon, sunset, and photo-light rows |
| TimeSettleText.tsx | Count-up page-turn animation for primary Today time values |
| FadeInOnReplay.tsx | Shared first-load and tap-replay value reveal treatment |
| StartupContext.tsx | Coordinates launch overlay, store hydration, and Today animation readiness |
| EInkSwitch.tsx / SegControl.tsx | Custom Settings controls for the current e-ink interface |
Tech Stack
| Mobile framework | React Native 0.81.1, React 19.1 |
| Language | TypeScript |
| UI foundation | React Native Paper provider, custom e-ink controls, custom styles, custom fonts |
| Navigation | React Navigation bottom tabs |
| State and persistence | Zustand with AsyncStorage-backed location and settings persistence |
| Date/time handling | Luxon plus native Date values from calculation libraries |
| Astronomy calculations | astronomy-engine |
| Halachic calculations | @hebcal/core |
| Location | @react-native-community/geolocation |
| Geocoding | OpenStreetMap Nominatim |
| Timezone resolution | tz-lookup |
| Graphics | react-native-svg |
| Testing | Jest, React Test Renderer, TypeScript checks, lint, standalone calculation diagnostics |
Engineering Decisions
Today-first product shape
The month calendar is no longer the only center of gravity. Today leads with the information a user wants while standing outside or planning the light: moon phase, moon age, moonrise, moonset, sun position, blue and golden hour, and Zmanim. Calendar remains the deeper planning view.
Fault-tolerant data merging
Astronomy and halachic data are fetched separately and merged after each path completes. Individual Zmanim
values are isolated so a single bad calculation does not blank the card, and missing values return
null
rather than blocking the entire view.
One moon phase source
Principal moon phases now route through
moonPhaseEventService
as the canonical phase enumeration path. Calendar markers, moon summaries, and upcoming data all draw from the
same service so phase times cannot diverge between parts of the interface.
Responsive Calendar orchestration
The Calendar screen uses memory-only caches, promise dedupe, stale-key guards, and cooperative prefetching. Visible phase and eclipse markers are warmed before below-the-fold sections because calendar cells are what users scan while navigating.
Timezone-aware location handling
GPS, coordinate, and manual search locations are enriched with IANA timezone IDs through offline
tz-lookup
resolution. Display times, phase and eclipse date assignment, and Hebcal calculations use the selected
location timezone when available.
Rendering polish without calculation drift
The current animation and visual work is presentation-only. The sky arc, marker clipping, page-turn time values, launch overlay, bottom-tab clearance, and app icon updates preserve the underlying astronomy, eclipse, photography-light, and Zmanim engine behavior.
Testing and Verification
The latest documented verification snapshot reports the core checks passing cleanly:
-
npm test -- --runInBand— Jest and React Native render/import coverage -
npm run lint— ESLint over active source with reference/prototype folders excluded -
npx tsc --noEmit— TypeScript verification -
npm run diagnostics— optional calculation smoke checks for astronomy, Hebcal, edge cases, and celestial APIs
Current Caveats
- Geocoding requires network access and depends on Nominatim availability and rate limits
- Elevation is fixed at 0 in current astronomy and halachic calculations
- Timezone boundary data should stay current through dependency updates because political timezone rules can change
- First Quarter and Last Quarter times are calculated, but the Upcoming board intentionally remains a product decision
- Android release signing uses private local credentials that must remain outside tracked git files
Roadmap
- Decide whether Upcoming should show all four principal moon phases or remain focused on New and Full Moon
- Make Android Node resolution portable instead of relying on a machine-specific path
- Add formal release automation, likely fastlane, around the current local beta upload workflow
- Add a production geocoding strategy with request throttling and usage safeguards
- Refresh portfolio imagery with current moondial e-ink screenshots after the visual set is finalized
Attribution
- Astronomy Engine by Don Cross — astronomical calculations including rise/set events, solar altitude, moon phases, and eclipse detection
- Hebcal Core — halachic Zmanim calculations
- bachtogauss.com/calendar — original web reference implementation