Skip to content

Commit 64cc7b3

Browse files
author
Lalit Sharma
committed
chore: add husky and lint-staged for pre-commit hooks
- Added husky as a dev dependency to manage Git hooks. - Configured lint-staged to run biome checks on staged files. - Updated package.json scripts to include a prepare script for husky. - Updated pnpm-lock.yaml to include new dependencies and their versions.
1 parent a308cee commit 64cc7b3

8 files changed

Lines changed: 362 additions & 41 deletions

File tree

.husky/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pnpm lint-staged

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
20

CHANGELOG.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [1.0.0] — 2026-02-16
9+
10+
### Added
11+
- **Eclipse catalog** — 200+ solar eclipses (1900–2100) with Besselian element polynomials, sourced from NASA data.
12+
- **Eclipse engine** — computes five contact points (C1/C2/max/C3/C4), magnitude, obscuration, and eclipse kind for any observer location.
13+
- **Interactive map** — tap or drag a pin to set observer location; toggle satellite/hybrid views.
14+
- **Overlay paths** — penumbral and umbral/antumbral shadow paths rendered on the map, loaded lazily by decade.
15+
- **GPS location** — one-tap "Use GPS" with pre-permission rationale dialog; location stays on-device.
16+
- **Live countdown** — ticking countdown to the next eclipse contact event.
17+
- **Local time display** — contact times shown in both UTC and device-local time.
18+
- **Search & filter** — tokenized search on the eclipse landing list (by year, date, kind, ID).
19+
- **Notifications** — per-contact local notification scheduling with configurable reminder lead times, sound, vibration, and TTS audio modes.
20+
- **Notification settings** — dedicated screen for global and per-eclipse notification preferences, persisted via AsyncStorage.
21+
- **NASA GIF preview** — prefetched/cached eclipse animation preview with loading placeholder and error fallback.
22+
- **Error boundary** — catches runtime crashes with a recovery UI and Sentry reporting.
23+
- **Sentry crash reporting**`@sentry/react-native` integration (production-only).
24+
- **Splash screen control** — native splash held until catalog loads via `expo-splash-screen`.
25+
- **Accessibility** — labels, roles, and states on all interactive elements (landing + notification settings screens).
26+
- **Privacy policy**`PRIVACY_POLICY.md` covering location, Sentry, NASA GIF, notifications; iOS privacy manifest in `app.json`.
27+
- **Store metadata** — descriptions, keywords, content rating answers, icon (alpha-stripped for iOS) in `documents/store-metadata.md`.
28+
- **CI/CD** — GitHub Actions CI (typecheck/lint/test), EAS Build + Submit workflow, `expo-updates` OTA support.
29+
- **Pre-commit hooks** — Husky + lint-staged running Biome check on staged files.
30+
- **EAS Build profiles**`development`, `preview`, `production` in `eas.json`.
31+
32+
### Changed
33+
- Refactored `App.tsx` from ~1 000-line god component into screens, hooks, and utilities.
34+
- Replaced `ScrollView` + `.map()` with virtualized `FlatList` on landing screen.
35+
- Engine magnitude formula uses geometric `(L1obs - Δ) / (L1obs + L2obs)` instead of hardcoded `1`.
36+
- Overlay polygons loaded lazily by decade instead of monolithic JSON.
37+
- `evaluateAtT` results cached per-`t` during contact solving to avoid duplicate evaluations.
38+
39+
### Fixed
40+
- Countdown timer now ticks in real time (was static).
41+
- GPS altitude wired through to engine `elevM` (was hardcoded to 0).

apps/mobile/.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Environment variables for Eclipse Timer
2+
# Copy this file to .env and fill in real values before production builds.
3+
4+
# Sentry — crash reporting (used in App.tsx Sentry.init)
5+
SENTRY_DSN=
6+
SENTRY_ORG=
7+
SENTRY_PROJECT=
8+
9+
# EAS — set automatically by EAS Build, but useful locally
10+
EXPO_PROJECT_ID=a29a7662-96be-4509-a79e-fbe4b5dac1ff

apps/mobile/src/hooks/useTimerState.ts

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
2-
import { Animated, Alert, InteractionManager } from "react-native";
3-
import type { Details, MapPressEvent, Region } from "react-native-maps";
4-
import type MapView from "react-native-maps";
5-
import * as Location from "expo-location";
6-
71
import { computeCircumstances } from "@eclipse-timer/engine";
82
import type { Circumstances, EclipseRecord, Observer } from "@eclipse-timer/shared";
9-
3+
import * as Location from "expo-location";
4+
import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
5+
import { Alert, Animated, InteractionManager } from "react-native";
6+
import type MapView from "react-native-maps";
7+
import type { Details, MapPressEvent, Region } from "react-native-maps";
8+
import {
9+
type NotificationEntry,
10+
type NotificationSettings,
11+
notificationEntryId,
12+
} from "../state/appState";
1013
import {
1114
buildContactItems,
12-
nextEventCountdown,
1315
type ContactItem,
1416
type ContactKey,
17+
nextEventCountdown,
1518
} from "../utils/contacts";
1619
import {
1720
normalizeLongitude,
@@ -20,17 +23,12 @@ import {
2023
sanitizeLatitude,
2124
sanitizeRegion,
2225
} from "../utils/map";
23-
import {
24-
notificationEntryId,
25-
type NotificationEntry,
26-
type NotificationSettings,
27-
} from "../state/appState";
2826

2927
type MapType3 = "standard" | "satellite" | "hybrid";
3028

3129
type AlarmState = Record<ContactKey, boolean>;
3230

33-
type Pin = { lat: number; lon: number };
31+
type Pin = { lat: number; lon: number; elevM: number };
3432
type MarkerDragEndEvent = {
3533
nativeEvent: {
3634
coordinate: {
@@ -40,7 +38,7 @@ type MarkerDragEndEvent = {
4038
};
4139
};
4240

43-
const GIBRALTAR = { lat: 36.1408, lon: -5.3536 };
41+
const GIBRALTAR: Pin = { lat: 36.1408, lon: -5.3536, elevM: 0 };
4442
const MIN_REGION_DIFF = 0.00001;
4543
const MIN_PIN_DIFF = 0.000001;
4644
const EMPTY_ALARM_STATE: AlarmState = {
@@ -66,7 +64,9 @@ function hasMeaningfulRegionChange(prev: Region, next: Region): boolean {
6664
}
6765

6866
function hasMeaningfulPinChange(prev: Pin, next: Pin): boolean {
69-
return Math.abs(prev.lat - next.lat) > MIN_PIN_DIFF || Math.abs(prev.lon - next.lon) > MIN_PIN_DIFF;
67+
return (
68+
Math.abs(prev.lat - next.lat) > MIN_PIN_DIFF || Math.abs(prev.lon - next.lon) > MIN_PIN_DIFF
69+
);
7070
}
7171

7272
export type TimerState = {
@@ -114,7 +114,7 @@ export function useTimerState(
114114
removeNotificationEntry: (id: string) => void,
115115
): TimerState {
116116
const mapRef = useRef<MapView>(null);
117-
const [pin, setPin] = useState<Pin>({ lat: GIBRALTAR.lat, lon: GIBRALTAR.lon });
117+
const [pin, setPin] = useState<Pin>({ lat: GIBRALTAR.lat, lon: GIBRALTAR.lon, elevM: 0 });
118118
const [mapType, setMapType] = useState<MapType3>("standard");
119119
const [showVisibleOverlay, setShowVisibleOverlay] = useState(true);
120120
const [showCentralOverlay, setShowCentralOverlay] = useState(true);
@@ -219,11 +219,11 @@ export function useTimerState(
219219
setShowDirectionsOverlay((prev) => !prev);
220220
};
221221

222-
const jumpTo = (lat: number, lon: number, delta = 3) => {
222+
const jumpTo = (lat: number, lon: number, delta = 3, elevM = 0) => {
223223
const safeLat = sanitizeLatitude(lat);
224224
const safeLon = normalizeLongitude(lon);
225225
const safeDelta = sanitizeDelta(delta, 3);
226-
const nextPin: Pin = { lat: safeLat, lon: safeLon };
226+
const nextPin: Pin = { lat: safeLat, lon: safeLon, elevM };
227227
const nextRegion: Region = {
228228
latitude: safeLat,
229229
longitude: safeLon,
@@ -250,7 +250,7 @@ export function useTimerState(
250250
const movePinKeepZoom = (lat: number, lon: number) => {
251251
const safeLat = sanitizeLatitude(lat);
252252
const safeLon = normalizeLongitude(lon);
253-
const nextPin: Pin = { lat: safeLat, lon: safeLon };
253+
const nextPin: Pin = { lat: safeLat, lon: safeLon, elevM: 0 };
254254
const pinMoved = hasMeaningfulPinChange(pin, nextPin);
255255

256256
if (pinMoved) {
@@ -310,7 +310,8 @@ export function useTimerState(
310310

311311
const last = await Location.getLastKnownPositionAsync();
312312
if (last?.coords) {
313-
jumpTo(last.coords.latitude, last.coords.longitude, 2);
313+
const alt = last.coords.altitude ?? 0;
314+
jumpTo(last.coords.latitude, last.coords.longitude, 2, alt);
314315
setStatus("Pin set from last known location");
315316
}
316317

@@ -330,7 +331,8 @@ export function useTimerState(
330331
]);
331332

332333
if (current?.coords) {
333-
jumpTo(current.coords.latitude, current.coords.longitude, 2);
334+
const alt = current.coords.altitude ?? 0;
335+
jumpTo(current.coords.latitude, current.coords.longitude, 2, alt);
334336
setStatus("Pin set from GPS");
335337
} else if (!last) {
336338
setStatus("GPS timed out (try again or move near a window)");
@@ -346,7 +348,7 @@ export function useTimerState(
346348
return;
347349
}
348350

349-
const observer: Observer = { latDeg: pin.lat, lonDeg: pin.lon, elevM: 0 };
351+
const observer: Observer = { latDeg: pin.lat, lonDeg: pin.lon, elevM: pin.elevM };
350352

351353
cancelPendingCompute();
352354
const runToken = computeRunTokenRef.current;
@@ -364,7 +366,7 @@ export function useTimerState(
364366
if (computeRunTokenRef.current !== runToken) return;
365367

366368
setResult(out);
367-
setResultPin({ lat: observer.latDeg, lon: observer.lonDeg });
369+
setResultPin({ lat: observer.latDeg, lon: observer.lonDeg, elevM: observer.elevM ?? 0 });
368370
setStatus("Computed");
369371

370372
resultFlash.setValue(0);

0 commit comments

Comments
 (0)