Update lint and typecheck scripts for all packages
Add linting to the mobile app and include core and api-client packages in the root lint, typecheck, and test scripts. Ignore node_modules in all subdirectories and clean up stale test results. Update lint and typecheck scripts for all packages Add ESLint configuration for mobile app and shared packages. Rename mobile jest config to .cjs and enable ESM. Include packages/core and packages/api-client in monorepo lint, typecheck, and test scripts.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import js from "@eslint/js";
|
||||
import ts from "typescript-eslint";
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
|
||||
// We have no .eslintrc, so we must define everything here.
|
||||
|
||||
export default ts.config(
|
||||
{
|
||||
ignores: ["dist/**"],
|
||||
},
|
||||
{
|
||||
extends: [js.configs.recommended, ts.configs.recommended],
|
||||
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
// React Native globals
|
||||
__DEV__: "readonly",
|
||||
alert: "readonly",
|
||||
console: "readonly",
|
||||
document: "readonly",
|
||||
navigator: "readonly",
|
||||
window: "readonly",
|
||||
require: "readonly",
|
||||
module: "readonly",
|
||||
exports: "readonly",
|
||||
process: "readonly",
|
||||
jest: "readonly",
|
||||
describe: "readonly",
|
||||
it: "readonly",
|
||||
test: "readonly",
|
||||
expect: "readonly",
|
||||
beforeEach: "readonly",
|
||||
afterEach: "readonly",
|
||||
beforeAll: "readonly",
|
||||
afterAll: "readonly",
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...ts.configs.recommended.rules,
|
||||
// TypeScript handles type-related issues
|
||||
"no-undef": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// Allow _-prefixed parameters and variables to signal intentionally unused
|
||||
"no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
varsIgnorePattern: "^_",
|
||||
argsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
varsIgnorePattern: "^_",
|
||||
argsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
// React Native uses require() heavily
|
||||
"no-var-requires": "off",
|
||||
// We use React Native's StyleSheet
|
||||
"react/no-unknown-property": [
|
||||
"error",
|
||||
{
|
||||
ignore: ["flex", "justifyContent", "alignItems", "width", "height", "margin", "padding"],
|
||||
},
|
||||
],
|
||||
// Allow console.log for debugging
|
||||
"no-console": "off",
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@timetoleave/mobile",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
@@ -8,7 +9,7 @@
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
||||
"lint": "echo 'no lint yet'",
|
||||
"lint": "eslint src/",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -28,14 +29,17 @@
|
||||
"react-native-screens": "^4.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/react": "^19",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~54.0.0",
|
||||
"react-test-renderer": "19.2.4",
|
||||
"ts-jest": "^29.4.9",
|
||||
"typescript": "~5.9.2"
|
||||
"typescript": "~5.9.2",
|
||||
"typescript-eslint": "^8.59.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { Event, Journey } from '@timetoleave/core';
|
||||
|
||||
describe('notifications service', () => {
|
||||
describe('calculateLeaveByTime', () => {
|
||||
it('should calculate leave-by time from event time minus buffer', () => {
|
||||
it('should calculate leave-by time from event time minus arrival buffer minus reminder buffer', () => {
|
||||
const event: Event = {
|
||||
id: 'test-1',
|
||||
title: 'Test Event',
|
||||
@@ -32,10 +32,12 @@ describe('notifications service', () => {
|
||||
source: 'manual',
|
||||
};
|
||||
|
||||
const leaveByTime = calculateLeaveByTime(event, [], 5, 30);
|
||||
const leaveByTime = calculateLeaveByTime(event, [], 30, 5);
|
||||
|
||||
// Leave-by time should be 30 minutes before event time
|
||||
const expectedTime = new Date('2025-01-01T09:30:00Z');
|
||||
// (arrival buffer) minus 5 minutes reminder buffer
|
||||
// = event time - 35 minutes total
|
||||
const expectedTime = new Date('2025-01-01T09:25:00Z');
|
||||
expect(leaveByTime.getTime()).toBe(expectedTime.getTime());
|
||||
});
|
||||
|
||||
@@ -75,10 +77,11 @@ describe('notifications service', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const leaveByTime = calculateLeaveByTime(event, journeys, 5, 30);
|
||||
const leaveByTime = calculateLeaveByTime(event, journeys, 30, 5);
|
||||
|
||||
// Should use earliest non-cancelled journey (journey-2 at 07:00) minus buffer
|
||||
const expectedTime = new Date('2025-01-01T06:30:00Z');
|
||||
// Should use earliest non-cancelled journey (journey-2 at 07:00)
|
||||
// Leave-by time = journey departure (07:00) - reminder buffer (5 min)
|
||||
const expectedTime = new Date('2025-01-01T06:55:00Z');
|
||||
expect(leaveByTime.getTime()).toBe(expectedTime.getTime());
|
||||
});
|
||||
|
||||
@@ -118,10 +121,11 @@ describe('notifications service', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const leaveByTime = calculateLeaveByTime(event, journeys, 5, 30);
|
||||
const leaveByTime = calculateLeaveByTime(event, journeys, 30, 5);
|
||||
|
||||
// Should use journey-2 since journey-1 is cancelled
|
||||
const expectedTime = new Date('2025-01-01T06:30:00Z');
|
||||
// Leave-by time = journey departure (07:00) - reminder buffer (5 min)
|
||||
const expectedTime = new Date('2025-01-01T06:55:00Z');
|
||||
expect(leaveByTime.getTime()).toBe(expectedTime.getTime());
|
||||
});
|
||||
|
||||
@@ -149,10 +153,11 @@ describe('notifications service', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const leaveByTime = calculateLeaveByTime(event, journeys, 5, 30);
|
||||
const leaveByTime = calculateLeaveByTime(event, journeys, 30, 5);
|
||||
|
||||
// All journeys cancelled, fall back to event time minus buffer
|
||||
const expectedTime = new Date('2025-01-01T09:30:00Z');
|
||||
// All journeys cancelled, fall back to event time minus arrival buffer minus reminder buffer
|
||||
// = 10:00 - 30 min - 5 min = 09:25
|
||||
const expectedTime = new Date('2025-01-01T09:25:00Z');
|
||||
expect(leaveByTime.getTime()).toBe(expectedTime.getTime());
|
||||
});
|
||||
|
||||
@@ -165,9 +170,12 @@ describe('notifications service', () => {
|
||||
source: 'manual',
|
||||
};
|
||||
|
||||
const leaveByTime = calculateLeaveByTime(event, [], 5, 0);
|
||||
const leaveByTime = calculateLeaveByTime(event, [], 30, 0);
|
||||
|
||||
expect(leaveByTime.getTime()).toBe(event.eventTime.getTime());
|
||||
// With zero reminder buffer, leave-by time = event time - arrival buffer
|
||||
// = 10:00 AM - 30 minutes = 09:30 AM
|
||||
const expectedTime = new Date('2025-01-01T09:30:00Z');
|
||||
expect(leaveByTime.getTime()).toBe(expectedTime.getTime());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
Text,
|
||||
|
||||
@@ -29,7 +29,7 @@ type ScreenProps = {
|
||||
route: RouteProp<RootStack, 'Settings'>;
|
||||
};
|
||||
|
||||
export function SettingsScreen({ navigation }: ScreenProps) {
|
||||
export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
|
||||
const [origin, setOrigin] = useState<Station | null>(null);
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState<Station[]>([]);
|
||||
@@ -101,29 +101,30 @@ export function SettingsScreen({ navigation }: ScreenProps) {
|
||||
const userLat = loc.coords.latitude;
|
||||
const userLng = loc.coords.longitude;
|
||||
|
||||
// Search for stations near the user's actual GPS coordinates
|
||||
// Use Nominatim reverse geocode via the API to find a nearby station
|
||||
const geoResults = await api.reverseGeocode(userLat, userLng);
|
||||
// Find the station closest to the user's actual coordinates
|
||||
if (geoResults.length > 0) {
|
||||
const closest = geoResults.reduce((best: { lat: number; lng: number; display_name: string } | null, candidate) => {
|
||||
if (!best) return candidate;
|
||||
const bestDist = Math.hypot(best.lat - userLat, best.lng - userLng);
|
||||
const candDist = Math.hypot(candidate.lat - userLat, candidate.lng - userLng);
|
||||
return candDist < bestDist ? candidate : best;
|
||||
}, null);
|
||||
|
||||
if (closest) {
|
||||
const station: Station = {
|
||||
name: closest.display_name,
|
||||
extId: String(closest.lat) + ',' + String(closest.lng),
|
||||
lat: closest.lat,
|
||||
lng: closest.lng,
|
||||
};
|
||||
selectStation(station);
|
||||
}
|
||||
// Find real public-transport stops near the user's GPS coordinates
|
||||
// via the WienerLinien nearby-stops proxy.
|
||||
const stops = await api.findNearbyStops(userLat, userLng, 2000);
|
||||
if (stops.length === 0) {
|
||||
Alert.alert('Keine Station gefunden', 'Kein ÖPNV-Halt in der Nähe gefunden.');
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
// Pick the closest stop to the user's actual position
|
||||
const closest = stops.reduce((best, candidate) => {
|
||||
const bestDist = Math.hypot(best.lat - userLat, best.lng - userLng);
|
||||
const candDist = Math.hypot(candidate.lat - userLat, candidate.lng - userLng);
|
||||
return candDist < bestDist ? candidate : best;
|
||||
}, stops[0]);
|
||||
|
||||
// Build a Station with the real stop id as extId — HAFAS can look this up.
|
||||
const station: Station = {
|
||||
name: closest.name,
|
||||
extId: closest.id,
|
||||
lat: closest.lat,
|
||||
lng: closest.lng,
|
||||
};
|
||||
selectStation(station);
|
||||
} catch (_err) {
|
||||
Alert.alert('Fehler', 'Standort konnte nicht ermittelt werden.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,23 +29,22 @@ export function calculateLeaveByTime(
|
||||
bufferMinutes: number
|
||||
): Date {
|
||||
// Calculate target arrival time (event time minus arrival buffer)
|
||||
const targetArrivalTime = new Date(event.eventTime);
|
||||
targetArrivalTime.setMinutes(targetArrivalTime.getMinutes() - arrivalBufferMinutes);
|
||||
const targetArrivalTimeMs = event.eventTime.getTime() - arrivalBufferMinutes * 60 * 1000;
|
||||
|
||||
// If we have journeys, use the earliest non-cancelled real departure
|
||||
// If we have journeys, use the earliest non-cancelled real departure minus reminder buffer
|
||||
if (journeys.length > 0) {
|
||||
const best = journeys
|
||||
.filter((j) => !j.cancelled)
|
||||
.sort((a, b) => a.rD.getTime() - b.rD.getTime())[0];
|
||||
|
||||
if (best) {
|
||||
// Leave by time = target arrival time - buffer minutes
|
||||
return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000);
|
||||
// Leave by time = earliest real departure time - reminder buffer
|
||||
return new Date(best.rD.getTime() - bufferMinutes * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: event time minus arrival buffer minus buffer (no journey data)
|
||||
return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000);
|
||||
// Fallback: event time minus arrival buffer minus reminder (no journey data)
|
||||
return new Date(targetArrivalTimeMs - bufferMinutes * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user