Reverse-engineered ParkSmarter API client (TypeScript, live-verified) plus a de-Googled Expo/React Native app for GrapheneOS: MapLibre meter map with GPS, VisionCamera QR kiosk scanning with save/share, local session-expiry reminders, UnifiedPush wiring, and Gitea CI to publish signed APKs for Obtainium. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
import React, { useCallback, useState } from 'react';
|
|
import { RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
import { useFocusEffect } from '@react-navigation/native';
|
|
import { ps } from '@/api/client';
|
|
import type { ActiveSession, PastSession } from 'parksmarter-client';
|
|
|
|
export function SessionsScreen() {
|
|
const [active, setActive] = useState<ActiveSession[]>([]);
|
|
const [past, setPast] = useState<PastSession[]>([]);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
const load = useCallback(async () => {
|
|
setRefreshing(true);
|
|
try {
|
|
const [a, p] = await Promise.all([
|
|
ps.getActiveParkingSessions(),
|
|
ps.getPastParkingSessions({ currentPage: 1, pageSize: 20 }),
|
|
]);
|
|
setActive(a.ParkingSession ?? []);
|
|
setPast(p.Session ?? []);
|
|
} finally {
|
|
setRefreshing(false);
|
|
}
|
|
}, []);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
void load();
|
|
}, [load]),
|
|
);
|
|
|
|
return (
|
|
<ScrollView
|
|
contentContainerStyle={{ padding: 16 }}
|
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={load} />}
|
|
>
|
|
<Text style={styles.header}>Active</Text>
|
|
{active.length === 0 ? (
|
|
<Text style={styles.empty}>No active sessions.</Text>
|
|
) : (
|
|
active.map((s, i) => (
|
|
<View key={i} style={[styles.card, styles.activeCard]}>
|
|
<Text style={styles.zone}>{s.ZoneName ?? 'Session'}</Text>
|
|
<Text style={styles.meta}>
|
|
{s.SpaceName ?? s.Space ?? ''} · ends {s.EndTimeDisplay ?? s.EndTime ?? ''}
|
|
</Text>
|
|
</View>
|
|
))
|
|
)}
|
|
|
|
<Text style={[styles.header, { marginTop: 20 }]}>History</Text>
|
|
{past.length === 0 ? (
|
|
<Text style={styles.empty}>No past sessions.</Text>
|
|
) : (
|
|
past.map((s, i) => (
|
|
<View key={i} style={styles.card}>
|
|
<Text style={styles.zone}>{s.ZoneName ?? 'Session'}</Text>
|
|
<Text style={styles.meta}>
|
|
{s.StartTime ?? ''} · {s.Amount != null ? `$${s.Amount}` : ''}
|
|
</Text>
|
|
</View>
|
|
))
|
|
)}
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
header: { fontSize: 18, fontWeight: '700', marginBottom: 8 },
|
|
empty: { color: '#888', marginBottom: 8 },
|
|
card: { backgroundColor: '#f4f4f4', borderRadius: 10, padding: 14, marginBottom: 10 },
|
|
activeCard: { backgroundColor: '#e8f5e9' },
|
|
zone: { fontSize: 16, fontWeight: '600' },
|
|
meta: { color: '#777', fontSize: 13, marginTop: 2 },
|
|
});
|