From 9a917cf70ef61705c58d94c246e95a38492b7ad4 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 7 Apr 2026 17:50:08 +0000
Subject: [PATCH] Refactor FlightScreen by extracting FlightCard component
Extracted the large `renderFlight` inline component and related helper views (`LogoPill`, `SwipeableFlightCard`) into a new dedicated `FlightCard` component inside `src/components/FlightCard.tsx`. This significantly reduces the size and complexity of `FlightScreen.tsx`, making the file much easier to read and maintain. No functionality or behavior was changed during the extraction.
Co-authored-by: TargetMisser <52361977+TargetMisser@users.noreply.github.com>
---
package-lock.json | 51 +++---
src/components/FlightCard.tsx | 299 ++++++++++++++++++++++++++++++++++
src/screens/FlightScreen.tsx | 293 ++-------------------------------
3 files changed, 337 insertions(+), 306 deletions(-)
create mode 100644 src/components/FlightCard.tsx
diff --git a/package-lock.json b/package-lock.json
index a3bbe99..9b54474 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
- "@react-native-picker/picker": "2.11.1",
+ "@react-native-picker/picker": "2.11.4",
"@types/tesseract.js": "^0.0.2",
"expo": "~54.0.0",
"expo-blur": "~15.0.8",
@@ -23,18 +23,19 @@
"expo-linear-gradient": "~15.0.8",
"expo-location": "~19.0.8",
"expo-notifications": "~0.32.16",
+ "expo-secure-store": "~15.0.5",
"expo-status-bar": "~3.0.9",
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-android-widget": "^0.20.1",
"react-native-calendars": "^1.1314.0",
- "react-native-webview": "13.15.0",
+ "react-native-webview": "13.16.1",
"tesseract.js": "^7.0.0"
},
"devDependencies": {
"@react-native-community/cli": "^20.1.3",
"@types/react": "~19.1.10",
- "pdfjs-dist": "^5.5.207",
+ "pdfjs-dist": "^5.6.205",
"typescript": "~5.9.2"
}
},
@@ -80,7 +81,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1492,7 +1492,6 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -2431,7 +2430,6 @@
"integrity": "sha512-sLo8cu9JyFNfuuF1C+8NJ4DHE/PEFaXGd4enkcxi/OJjGG8+sOQrdjNQ4i+cVh/2c+ah1mEMwsYjc3z0+/MqSg==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@react-native-community/cli-clean": "20.1.3",
"@react-native-community/cli-config": "20.1.3",
@@ -2996,9 +2994,9 @@
}
},
"node_modules/@react-native-picker/picker": {
- "version": "2.11.1",
- "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.1.tgz",
- "integrity": "sha512-ThklnkK4fV3yynnIIRBkxxjxR4IFbdMNJVF6tlLdOJ/zEFUEFUEdXY0KmH0iYzMwY8W4/InWsLiA7AkpAbnexA==",
+ "version": "2.11.4",
+ "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.4.tgz",
+ "integrity": "sha512-Kf8h1AMnBo54b1fdiVylP2P/iFcZqzpMYcglC28EEFB1DEnOjsNr6Ucqc+3R9e91vHxEDnhZFbYDmAe79P2gjA==",
"license": "MIT",
"workspaces": [
"example"
@@ -3399,7 +3397,6 @@
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4109,7 +4106,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5014,7 +5010,6 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz",
"integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "54.0.23",
@@ -5130,7 +5125,6 @@
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
"integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
@@ -5239,6 +5233,15 @@
"react-native": "*"
}
},
+ "node_modules/expo-secure-store": {
+ "version": "15.0.8",
+ "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.8.tgz",
+ "integrity": "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-server": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz",
@@ -8412,16 +8415,16 @@
}
},
"node_modules/pdfjs-dist": {
- "version": "5.5.207",
- "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz",
- "integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==",
+ "version": "5.6.205",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.6.205.tgz",
+ "integrity": "sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=20.19.0 || >=22.13.0 || >=24"
},
"optionalDependencies": {
- "@napi-rs/canvas": "^0.1.95",
+ "@napi-rs/canvas": "^0.1.96",
"node-readable-to-web-readable-stream": "^0.4.2"
}
},
@@ -8436,7 +8439,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -8716,7 +8718,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8742,7 +8743,6 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.81.5",
@@ -8849,11 +8849,10 @@
"license": "MIT"
},
"node_modules/react-native-webview": {
- "version": "13.15.0",
- "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz",
- "integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==",
+ "version": "13.16.1",
+ "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.1.tgz",
+ "integrity": "sha512-If0eHhoEdOYDcHsX+xBFwHMbWBGK1BvGDQDQdVkwtSIXiq1uiqjkpWVP2uQ1as94J0CzvFE9PUNDuhiX0Z6ubw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"escape-string-regexp": "^4.0.0",
"invariant": "2.2.4"
@@ -8952,7 +8951,6 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -10211,9 +10209,8 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/src/components/FlightCard.tsx b/src/components/FlightCard.tsx
new file mode 100644
index 0000000..ab5694b
--- /dev/null
+++ b/src/components/FlightCard.tsx
@@ -0,0 +1,299 @@
+import React, { useState, useRef, useMemo } from 'react';
+import { View, Text, StyleSheet, Image, Animated, PanResponder } from 'react-native';
+import { MaterialIcons } from '@expo/vector-icons';
+import { useAppTheme, type ThemeColors } from '../context/ThemeContext';
+import { useLanguage } from '../context/LanguageContext';
+import { getAirlineOps, getAirlineColor } from '../utils/airlineOps';
+import { normalizeFlightNumber, type StaffMonitorFlight } from '../utils/staffMonitor';
+
+function LogoPill({ iataCode, airlineName, color }: { iataCode: string; airlineName: string; color: string }) {
+ const [err, setErr] = useState(false);
+ const uri = `https://pics.avs.io/160/60/${(iataCode || '').toUpperCase()}.png`;
+ const initials = airlineName.split(' ').slice(0, 2).map(w => w[0]).join('').toUpperCase();
+ if (iataCode && !err) {
+ return (
+
+ setErr(true)} />
+
+ );
+ }
+ return (
+
+ {initials}
+
+ );
+}
+
+const SWIPE_THRESHOLD = 80;
+
+function SwipeableFlightCard({
+ children, isPinned, onToggle,
+}: {
+ children: React.ReactNode;
+ isPinned: boolean;
+ onToggle: () => void;
+}) {
+ const translateX = useRef(new Animated.Value(0)).current;
+ const onToggleRef = useRef(onToggle);
+ onToggleRef.current = onToggle;
+
+ const panResponder = useMemo(() => PanResponder.create({
+ onMoveShouldSetPanResponder: (_, g) =>
+ Math.abs(g.dx) > 15 && Math.abs(g.dx) > Math.abs(g.dy) * 1.5,
+ onPanResponderMove: (_, g) => {
+ if (g.dx < 0) translateX.setValue(g.dx);
+ },
+ onPanResponderRelease: (_, g) => {
+ if (g.dx < -SWIPE_THRESHOLD) {
+ Animated.timing(translateX, { toValue: -SWIPE_THRESHOLD, duration: 100, useNativeDriver: true }).start(() => {
+ onToggleRef.current();
+ Animated.spring(translateX, { toValue: 0, useNativeDriver: true, tension: 120, friction: 10 }).start();
+ });
+ } else {
+ Animated.spring(translateX, { toValue: 0, useNativeDriver: true, tension: 120, friction: 10 }).start();
+ }
+ },
+ onPanResponderTerminate: () => {
+ Animated.spring(translateX, { toValue: 0, useNativeDriver: true }).start();
+ },
+ }), []);
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export interface FlightCardProps {
+ item: any;
+ activeTab: 'arrivals' | 'departures';
+ userShift: { start: number; end: number } | null | undefined;
+ pinnedFlightId: string | null;
+ pinFlight: (item: any) => void;
+ unpinFlight: () => void;
+ inboundArrivals: Record;
+ staffMonitorDeps: StaffMonitorFlight[];
+ staffMonitorArrs: StaffMonitorFlight[];
+}
+
+export function FlightCard({
+ item,
+ activeTab,
+ userShift,
+ pinnedFlightId,
+ pinFlight,
+ unpinFlight,
+ inboundArrivals,
+ staffMonitorDeps,
+ staffMonitorArrs
+}: FlightCardProps) {
+ const { colors } = useAppTheme();
+ const { t, locale } = useLanguage();
+ const s = useMemo(() => makeStyles(colors), [colors]);
+
+ const flightNumber = item.flight?.identification?.number?.default || 'N/A';
+ const airline = item.flight?.airline?.name || 'Sconosciuta';
+ const iataCode = item.flight?.airline?.code?.iata || '';
+ const statusText = item.flight?.status?.text || 'Scheduled';
+ const raw = item.flight?.status?.generic?.status?.color || 'gray';
+ const statusColor = raw === 'green' ? '#10b981' : raw === 'red' ? '#ef4444' : raw === 'yellow' ? '#f59e0b' : '#6b7280';
+ const originDest = activeTab === 'arrivals'
+ ? (item.flight?.airport?.origin?.name || item.flight?.airport?.origin?.code?.iata || 'N/A')
+ : (item.flight?.airport?.destination?.name || item.flight?.airport?.destination?.code?.iata || 'N/A');
+ const ts = activeTab === 'arrivals' ? item.flight?.time?.scheduled?.arrival : item.flight?.time?.scheduled?.departure;
+ const time = ts ? new Date(ts * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) : 'N/A';
+ const duringShift = userShift && ts && (() => {
+ if (activeTab === 'arrivals') return ts >= userShift.start && ts <= userShift.end;
+ const opsData = getAirlineOps(airline);
+ const ciOpen = ts - opsData.checkInOpen * 60;
+ const ciClose = ts - opsData.checkInClose * 60;
+ const gOpen = ts - opsData.gateOpen * 60;
+ const gClose = ts - opsData.gateClose * 60;
+ const ciOverlap = ciOpen <= userShift.end && ciClose >= userShift.start;
+ const gateOverlap = gOpen <= userShift.end && gClose >= userShift.start;
+ return ciOverlap || gateOverlap;
+ })();
+ const color = getAirlineColor(airline);
+ const ops = activeTab === 'departures' && ts ? getAirlineOps(airline) : null;
+ const fmt = (offsetMin: number) =>
+ ts ? new Date((ts - offsetMin * 60) * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) : '';
+ const fmtTs = (tValue: number) =>
+ new Date(tValue * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
+
+ const reg = item.flight?.aircraft?.registration;
+ const inboundTs = reg ? inboundArrivals[reg] : undefined;
+ const gateOpenFromInbound = activeTab === 'departures' && ts && inboundTs ? inboundTs : undefined;
+
+ const flightId = item.flight?.identification?.number?.default || null;
+ const isPinned = flightId !== null && flightId === pinnedFlightId;
+
+ const normFn = normalizeFlightNumber(flightNumber);
+ const normalizeForMatching = (str: string) => str.replace(/[\s\-_]/g, '').toUpperCase();
+ const normFnStripped = normalizeForMatching(normFn);
+ const smPool = activeTab === 'departures' ? staffMonitorDeps : staffMonitorArrs;
+ const smFlight =
+ smPool.find(sm => sm.flightNumber === normFn) ??
+ smPool.find(sm => normalizeForMatching(sm.flightNumber) === normFnStripped);
+
+ if (__DEV__ && !smFlight && smPool.length > 0) {
+ console.log(`[FlightCard] No staffMonitor match for "${normFn}" (stripped: "${normFnStripped}") in ${activeTab}`);
+ }
+
+ return (
+ isPinned ? unpinFlight() : pinFlight(item)}
+ >
+
+ {isPinned && {t('flightPinned')}}
+
+
+
+
+ {flightNumber}
+ {airline}
+
+
+
+ {time}
+ {originDest}
+
+
+
+ {activeTab === 'departures' && ops ? (
+
+
+
+
+ {t('flightCheckin')}
+ {fmt(ops.checkInOpen)} – {fmt(ops.checkInClose)}
+
+
+
+
+
+ {t('flightGate')}
+
+ {gateOpenFromInbound ? fmtTs(gateOpenFromInbound) : fmt(ops.gateOpen)} – {fmt(ops.gateClose)}
+
+
+
+
+ ) : activeTab === 'arrivals' && ts ? (() => {
+ const realDep = item.flight?.time?.real?.departure;
+ const realArr = item.flight?.time?.real?.arrival;
+ const estArr = item.flight?.time?.estimated?.arrival;
+ const bestArr = realArr || estArr || ts;
+ const delayMin = Math.round((bestArr - ts) / 60);
+ const landed = !!realArr;
+ const departed = !!realDep;
+
+ const landColor = landed ? '#10B981'
+ : delayMin > 20 ? '#EF4444'
+ : delayMin > 5 ? '#F59E0B'
+ : colors.primary;
+ const landLabel = landed ? t('flightLanded') : t('flightEstimated');
+
+ return (
+
+
+
+
+ {t('flightDeparted')}
+
+ {departed ? fmtTs(realDep) : '--:--'}
+
+
+
+
+
+
+ {landLabel}
+ {fmtTs(bestArr)}
+
+
+
+ );
+ })() : (
+ {`Da: ${originDest}`}
+ )}
+ {activeTab === 'arrivals' && ts ? (() => {
+ const rArr = item.flight?.time?.real?.arrival;
+ const eArr = item.flight?.time?.estimated?.arrival;
+ const bArr = rArr || eArr || ts;
+ const dMin = Math.round((bArr - ts) / 60);
+ const isLanded = !!rArr;
+ const dText = isLanded ? 'Atterrato' : dMin > 0 ? `+${dMin} min` : 'In orario';
+ const dColor = isLanded ? '#10B981' : dMin > 20 ? '#EF4444' : dMin > 5 ? '#F59E0B' : '#10B981';
+ return (
+
+ {dText}
+
+ );
+ })() : (
+
+ {statusText}
+
+ )}
+
+
+ {smFlight && (smFlight.stand || smFlight.checkin || smFlight.gate || smFlight.belt) && (
+
+ {smFlight.stand && (
+
+
+ Stand {smFlight.stand}
+
+ )}
+ {smFlight.checkin && (
+
+
+ {t('flightCheckin')} {smFlight.checkin}
+
+ )}
+ {smFlight.gate && (
+
+
+ {t('flightGate')} {smFlight.gate}
+
+ )}
+ {smFlight.belt && (
+
+
+ {t('flightBelt')} {smFlight.belt}
+
+ )}
+
+ )}
+
+ );
+}
+
+function makeStyles(c: ThemeColors) {
+ return StyleSheet.create({
+ card: { backgroundColor: c.card, borderRadius: 16, marginBottom: 10, overflow: 'hidden', shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, shadowRadius: 10, elevation: c.isDark ? 0 : 3, borderWidth: c.isDark ? 1 : 0, borderColor: c.glassBorder },
+ cardPinned: { borderWidth: 2, borderColor: '#F59E0B' },
+ pinBanner: { backgroundColor: '#F59E0B', paddingVertical: 5, paddingHorizontal: 12 },
+ pinBannerText: { color: '#fff', fontWeight: 'bold', fontSize: 11, letterSpacing: 0.5 },
+ statusPill: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 20, marginTop: 5 },
+ statusText: { fontSize: 10, fontWeight: '700' },
+ cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10, paddingHorizontal: 14 },
+ headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
+ headerFlightNum: { color: '#fff', fontWeight: '900', fontSize: 15, lineHeight: 18 },
+ headerAirlineName: { color: 'rgba(255,255,255,0.8)', fontSize: 10 },
+ headerTime: { color: '#fff', fontWeight: '900', fontSize: 18, lineHeight: 20, textAlign: 'right' },
+ headerDest: { color: 'rgba(255,255,255,0.8)', fontSize: 10, textAlign: 'right' },
+ cardBody: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10, paddingHorizontal: 14, backgroundColor: c.card },
+ bodyInfo: { flex: 1, fontSize: 11, color: c.textSub },
+ opsRow: { flex: 1, flexDirection: 'row', gap: 8 },
+ opsBadge: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: c.primaryLight, borderRadius: 10, paddingHorizontal: 10, paddingVertical: 8 },
+ opsLabel: { fontSize: 10, fontWeight: '600', color: c.textSub, letterSpacing: 0.5 },
+ opsTime: { fontSize: 13, fontWeight: '800', color: c.primaryDark },
+ smFooter: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, paddingHorizontal: 14, paddingBottom: 10, backgroundColor: c.card },
+ smPill: { flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: c.primaryLight, borderRadius: 8, paddingHorizontal: 8, paddingVertical: 4 },
+ smPillText: { fontSize: 11, fontWeight: '700', color: c.primaryDark },
+ });
+}
diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx
index 3a28fb0..630ccd7 100644
--- a/src/screens/FlightScreen.tsx
+++ b/src/screens/FlightScreen.tsx
@@ -12,8 +12,9 @@ import { useAppTheme, type ThemeColors } from '../context/ThemeContext';
import { useAirport } from '../context/AirportContext';
import { getAirlineOps, getAirlineColor } from '../utils/airlineOps';
import { fetchAirportScheduleRaw } from '../utils/fr24api';
-import { fetchStaffMonitorData, normalizeFlightNumber, type StaffMonitorFlight } from '../utils/staffMonitor';
+import { fetchStaffMonitorData, type StaffMonitorFlight } from '../utils/staffMonitor';
import { formatAirportHeader } from '../utils/airportSettings';
+import { FlightCard } from '../components/FlightCard';
import { requestWidgetUpdate } from 'react-native-android-widget';
import { WIDGET_CACHE_KEY } from '../widgets/widgetTaskHandler';
import type { WidgetData, WidgetFlight } from '../widgets/widgetTaskHandler';
@@ -40,67 +41,6 @@ try { Notifications.setNotificationHandler({
}); } catch (e) { if (__DEV__) console.warn('[notifHandler]', e); }
-function LogoPill({ iataCode, airlineName, color }: { iataCode: string; airlineName: string; color: string }) {
- const [err, setErr] = useState(false);
- const uri = `https://pics.avs.io/160/60/${(iataCode || '').toUpperCase()}.png`;
- const initials = airlineName.split(' ').slice(0, 2).map(w => w[0]).join('').toUpperCase();
- if (iataCode && !err) {
- return (
-
- setErr(true)} />
-
- );
- }
- return (
-
- {initials}
-
- );
-}
-
-const SWIPE_THRESHOLD = 80;
-
-function SwipeableFlightCard({
- children, isPinned, onToggle,
-}: {
- children: React.ReactNode;
- isPinned: boolean;
- onToggle: () => void;
-}) {
- const translateX = useRef(new Animated.Value(0)).current;
- const onToggleRef = useRef(onToggle);
- onToggleRef.current = onToggle;
-
- const panResponder = useMemo(() => PanResponder.create({
- onMoveShouldSetPanResponder: (_, g) =>
- Math.abs(g.dx) > 15 && Math.abs(g.dx) > Math.abs(g.dy) * 1.5,
- onPanResponderMove: (_, g) => {
- if (g.dx < 0) translateX.setValue(g.dx);
- },
- onPanResponderRelease: (_, g) => {
- if (g.dx < -SWIPE_THRESHOLD) {
- Animated.timing(translateX, { toValue: -SWIPE_THRESHOLD, duration: 100, useNativeDriver: true }).start(() => {
- onToggleRef.current();
- Animated.spring(translateX, { toValue: 0, useNativeDriver: true, tension: 120, friction: 10 }).start();
- });
- } else {
- Animated.spring(translateX, { toValue: 0, useNativeDriver: true, tension: 120, friction: 10 }).start();
- }
- },
- onPanResponderTerminate: () => {
- Animated.spring(translateX, { toValue: 0, useNativeDriver: true }).start();
- },
- }), []);
-
- return (
-
-
- {children}
-
-
- );
-}
-
// ─── Helpers notifiche ─────────────────────────────────────────────────────────
async function cancelPreviousNotifications() {
const raw = await AsyncStorage.getItem(NOTIF_IDS_KEY);
@@ -544,197 +484,20 @@ export default function FlightScreen() {
})();
const renderFlight = useCallback(({ item }: { item: any }) => {
- const flightNumber = item.flight?.identification?.number?.default || 'N/A';
- const airline = item.flight?.airline?.name || 'Sconosciuta';
- const iataCode = item.flight?.airline?.code?.iata || '';
- const statusText = item.flight?.status?.text || 'Scheduled';
- const raw = item.flight?.status?.generic?.status?.color || 'gray';
- const statusColor = raw === 'green' ? '#10b981' : raw === 'red' ? '#ef4444' : raw === 'yellow' ? '#f59e0b' : '#6b7280';
- const originDest = activeTab === 'arrivals'
- ? (item.flight?.airport?.origin?.name || item.flight?.airport?.origin?.code?.iata || 'N/A')
- : (item.flight?.airport?.destination?.name || item.flight?.airport?.destination?.code?.iata || 'N/A');
- const ts = activeTab === 'arrivals' ? item.flight?.time?.scheduled?.arrival : item.flight?.time?.scheduled?.departure;
- const time = ts ? new Date(ts * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) : 'N/A';
- const duringShift = userShift && ts && (() => {
- if (activeTab === 'arrivals') return ts >= userShift.start && ts <= userShift.end;
- // Departures: CI or Gate window overlaps with shift (even 1 min)
- const opsData = getAirlineOps(airline);
- const ciOpen = ts - opsData.checkInOpen * 60;
- const ciClose = ts - opsData.checkInClose * 60;
- const gOpen = ts - opsData.gateOpen * 60;
- const gClose = ts - opsData.gateClose * 60;
- const ciOverlap = ciOpen <= userShift.end && ciClose >= userShift.start;
- const gateOverlap = gOpen <= userShift.end && gClose >= userShift.start;
- return ciOverlap || gateOverlap;
- })();
- const color = getAirlineColor(airline);
- // ops is null when ts is falsy — fmt is only called when ops is truthy
- const ops = activeTab === 'departures' && ts ? getAirlineOps(airline) : null;
- const fmt = (offsetMin: number) =>
- ts ? new Date((ts - offsetMin * 60) * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) : '';
- const fmtTs = (t: number) =>
- new Date(t * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
-
- // Gate open = inbound aircraft arrival time (if available)
- const reg = item.flight?.aircraft?.registration;
- const inboundTs = reg ? inboundArrivals[reg] : undefined;
- const gateOpenFromInbound = activeTab === 'departures' && ts && inboundTs ? inboundTs : undefined;
-
- const flightId = item.flight?.identification?.number?.default || null;
- const isPinned = flightId !== null && flightId === pinnedFlightId;
-
- const normFn = normalizeFlightNumber(flightNumber);
- const normalizeForMatching = (s: string) => s.replace(/[\s\-_]/g, '').toUpperCase();
- const normFnStripped = normalizeForMatching(normFn);
- const smPool = activeTab === 'departures' ? staffMonitorDeps : staffMonitorArrs;
- const smFlight =
- smPool.find(sm => sm.flightNumber === normFn) ??
- smPool.find(sm => normalizeForMatching(sm.flightNumber) === normFnStripped);
- if (__DEV__ && !smFlight && smPool.length > 0) {
- console.log(`[FlightScreen] No staffMonitor match for "${normFn}" (stripped: "${normFnStripped}") in ${activeTab}`);
- }
-
return (
- isPinned ? unpinFlight() : pinFlight(item)}
- >
-
- {isPinned && {t('flightPinned')}}
- {/* Header */}
-
-
-
-
- {flightNumber}
- {airline}
-
-
-
- {time}
- {originDest}
-
-
- {/* Body */}
-
- {activeTab === 'departures' && ops ? (
-
-
-
-
- {t('flightCheckin')}
- {fmt(ops.checkInOpen)} – {fmt(ops.checkInClose)}
-
-
-
-
-
- {t('flightGate')}
-
- {gateOpenFromInbound ? fmtTs(gateOpenFromInbound) : fmt(ops.gateOpen)} – {fmt(ops.gateClose)}
-
-
-
-
- ) : activeTab === 'arrivals' && ts ? (() => {
- const realDep = item.flight?.time?.real?.departure;
- const realArr = item.flight?.time?.real?.arrival;
- const estArr = item.flight?.time?.estimated?.arrival;
- const bestArr = realArr || estArr || ts;
- const delayMin = Math.round((bestArr - ts) / 60);
- const landed = !!realArr;
- const departed = !!realDep;
-
- // Color logic for landing badge
- const landColor = landed ? '#10B981'
- : delayMin > 20 ? '#EF4444'
- : delayMin > 5 ? '#F59E0B'
- : colors.primary;
- const landLabel = landed ? t('flightLanded') : t('flightEstimated');
-
- // Delay pill
- const delayText = landed ? 'Atterrato'
- : delayMin > 0 ? `+${delayMin} min`
- : t('flightOnTime');
- const delayColor = landed ? '#10B981'
- : delayMin > 20 ? '#EF4444'
- : delayMin > 5 ? '#F59E0B'
- : '#10B981';
-
- return (
-
-
-
-
- {t('flightDeparted')}
-
- {departed ? fmtTs(realDep) : '--:--'}
-
-
-
-
-
-
- {landLabel}
- {fmtTs(bestArr)}
-
-
-
- );
- })() : (
- {`Da: ${originDest}`}
- )}
- {activeTab === 'arrivals' && ts ? (() => {
- const rArr = item.flight?.time?.real?.arrival;
- const eArr = item.flight?.time?.estimated?.arrival;
- const bArr = rArr || eArr || ts;
- const dMin = Math.round((bArr - ts) / 60);
- const isLanded = !!rArr;
- const dText = isLanded ? 'Atterrato' : dMin > 0 ? `+${dMin} min` : 'In orario';
- const dColor = isLanded ? '#10B981' : dMin > 20 ? '#EF4444' : dMin > 5 ? '#F59E0B' : '#10B981';
- return (
-
- {dText}
-
- );
- })() : (
-
- {statusText}
-
- )}
-
-
- {smFlight && (smFlight.stand || smFlight.checkin || smFlight.gate || smFlight.belt) && (
-
- {smFlight.stand && (
-
-
- Stand {smFlight.stand}
-
- )}
- {smFlight.checkin && (
-
-
- {t('flightCheckin')} {smFlight.checkin}
-
- )}
- {smFlight.gate && (
-
-
- {t('flightGate')} {smFlight.gate}
-
- )}
- {smFlight.belt && (
-
-
- {t('flightBelt')} {smFlight.belt}
-
- )}
-
- )}
-
+
);
- }, [activeTab, userShift, s, pinnedFlightId, pinFlight, unpinFlight, inboundArrivals, colors, staffMonitorDeps, staffMonitorArrs]);
+ }, [activeTab, userShift, pinnedFlightId, pinFlight, unpinFlight, inboundArrivals, staffMonitorDeps, staffMonitorArrs]);
return (
@@ -876,31 +639,6 @@ function makeStyles(c: ThemeColors) {
segBtnActive: { backgroundColor: c.card, borderWidth: 1, borderColor: c.primaryLight },
segBtnText: { fontSize: 12, fontWeight: '500', color: c.textSub },
segBtnTextActive: { color: c.primary, fontWeight: '700' },
- card: { backgroundColor: c.card, borderRadius: 16, marginBottom: 10, overflow: 'hidden', shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, shadowRadius: 10, elevation: c.isDark ? 0 : 3, borderWidth: c.isDark ? 1 : 0, borderColor: c.glassBorder },
- cardShift: { borderWidth: 1.5, borderColor: '#F59E0B' },
- shiftBanner: { backgroundColor: '#F59E0B', paddingVertical: 5, paddingHorizontal: 12 },
- shiftBannerText: { color: '#fff', fontWeight: 'bold', fontSize: 11, letterSpacing: 0.5 },
- cardPinned: { borderWidth: 2, borderColor: '#F59E0B' },
- pinBanner: { backgroundColor: '#F59E0B', paddingVertical: 5, paddingHorizontal: 12 },
- pinBannerText: { color: '#fff', fontWeight: 'bold', fontSize: 11, letterSpacing: 0.5 },
- statusPill: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 20, marginTop: 5 },
- statusText: { fontSize: 10, fontWeight: '700' },
- cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10, paddingHorizontal: 14 },
- headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
- headerFlightNum: { color: '#fff', fontWeight: '900', fontSize: 15, lineHeight: 18 },
- headerAirlineName: { color: 'rgba(255,255,255,0.8)', fontSize: 10 },
- headerTime: { color: '#fff', fontWeight: '900', fontSize: 18, lineHeight: 20, textAlign: 'right' },
- headerDest: { color: 'rgba(255,255,255,0.8)', fontSize: 10, textAlign: 'right' },
- cardBody: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10, paddingHorizontal: 14, backgroundColor: c.card },
- bodyInfo: { flex: 1, fontSize: 11, color: c.textSub },
- bodyTime: { fontWeight: '700', color: c.text },
- opsRow: { flex: 1, flexDirection: 'row', gap: 8 },
- opsBadge: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: c.primaryLight, borderRadius: 10, paddingHorizontal: 10, paddingVertical: 8 },
- opsIcon: { fontSize: 16 },
- opsLabel: { fontSize: 10, fontWeight: '600', color: c.textSub, letterSpacing: 0.5 },
- opsTime: { fontSize: 13, fontWeight: '800', color: c.primaryDark },
- pinBtn: { width: 34, height: 34, borderRadius: 17, backgroundColor: 'rgba(255,255,255,0.15)', justifyContent: 'center', alignItems: 'center' },
- pinBtnActive: { backgroundColor: 'rgba(245,158,11,0.25)' },
filterBtn: { width: 42, height: 42, borderRadius: 21, backgroundColor: c.cardSecondary, justifyContent: 'center', alignItems: 'center', marginRight: 8 },
filterBtnActive: { backgroundColor: c.primary, shadowColor: c.primary, shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.35, shadowRadius: 6, elevation: 5 },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.55)', justifyContent: 'flex-end' },
@@ -911,8 +649,5 @@ function makeStyles(c: ThemeColors) {
filterOptionActive: { backgroundColor: c.primaryLight, borderWidth: 1.5, borderColor: c.primaryLight },
filterOptionText: { fontSize: 15, fontWeight: '600', color: c.text },
filterOptionSub: { fontSize: 12, color: c.textSub, marginTop: 2 },
- smFooter: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, paddingHorizontal: 14, paddingBottom: 10, backgroundColor: c.card },
- smPill: { flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: c.primaryLight, borderRadius: 8, paddingHorizontal: 8, paddingVertical: 4 },
- smPillText: { fontSize: 11, fontWeight: '700', color: c.primaryDark },
});
}