Some checks failed
CI / Build & test backend (push) Failing after 14m56s
REQ-MOB-010: BarcodeScreen.tsx — barcode scanner via react-native-camera REQ-VIZ-001: WeeklyCalorieChart.tsx — 7-day bar chart on History screen REQ-VIZ-002: Streak tracker — GET /meals/streak + HomeScreen badge REQ-UX-001: Quick-add calories — POST /meals/quick-add + QuickAddScreen REQ-UX-002: Food favourites — UserFoodMemory.favourite + toggle endpoint + FoodRow star REQ-UX-003: GoalBanner.tsx — in-app slide-in when daily target hit REQ-EXP-001: ExportController — GET /export/meals CSV download REQ-WTR-001: Water tracking — WaterEntry entity + POST/GET /water + DailyDetails widget REQ-UX-004: Daily logging reminder — HomeScreen after-18:00 banner Also: Flyway V2 (favourite), V3 (water_entries), V4 (source constraints) Traceability, CHANGELOG, PLAN updated after each feature
91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
// Generated by GitHub Copilot
|
|
import React, { useEffect, useRef } from 'react';
|
|
import { Animated, Text, StyleSheet, AccessibilityInfo } from 'react-native';
|
|
import { Colors } from '../theme/colors';
|
|
import { Spacing } from '../theme/spacing';
|
|
|
|
interface GoalBannerProps {
|
|
/** When true the banner slides in; hides automatically after 4 seconds. */
|
|
visible: boolean;
|
|
}
|
|
|
|
/**
|
|
* Slide-in banner shown when the user reaches their daily calorie goal.
|
|
* Auto-dismisses after 4 seconds.
|
|
* In-app only — no native push notification required.
|
|
* REQ-UX-003
|
|
*
|
|
* Accessibility: announces the goal achievement via `AccessibilityInfo.announceForAccessibility`
|
|
* so screen readers hear it even though the banner is transient.
|
|
*/
|
|
export default function GoalBanner({ visible }: GoalBannerProps) {
|
|
const slideAnim = useRef(new Animated.Value(-80)).current;
|
|
const opacityAnim = useRef(new Animated.Value(0)).current;
|
|
|
|
useEffect(() => {
|
|
if (!visible) return;
|
|
|
|
// Announce for screen readers
|
|
AccessibilityInfo.announceForAccessibility("Goal reached! You've hit your daily calorie target.");
|
|
|
|
// Slide in
|
|
Animated.parallel([
|
|
Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, bounciness: 8 }),
|
|
Animated.timing(opacityAnim, { toValue: 1, duration: 200, useNativeDriver: true }),
|
|
]).start();
|
|
|
|
// Auto-dismiss after 4 seconds
|
|
const timer = setTimeout(() => {
|
|
Animated.parallel([
|
|
Animated.timing(slideAnim, { toValue: -80, duration: 300, useNativeDriver: true }),
|
|
Animated.timing(opacityAnim, { toValue: 0, duration: 300, useNativeDriver: true }),
|
|
]).start();
|
|
}, 4000);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [visible, slideAnim, opacityAnim]);
|
|
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<Animated.View
|
|
style={[
|
|
styles.banner,
|
|
{ transform: [{ translateY: slideAnim }], opacity: opacityAnim },
|
|
]}
|
|
accessible
|
|
accessibilityRole="alert"
|
|
accessibilityLabel="Goal reached! You've hit your daily calorie target."
|
|
>
|
|
<Text style={styles.text}>🎉 Goal reached! Daily target hit.</Text>
|
|
</Animated.View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
banner: {
|
|
position: 'absolute',
|
|
top: Spacing.sm,
|
|
left: Spacing.md,
|
|
right: Spacing.md,
|
|
backgroundColor: Colors.primary,
|
|
borderRadius: Spacing.borderRadius.md,
|
|
paddingHorizontal: Spacing.md,
|
|
paddingVertical: Spacing.sm,
|
|
zIndex: 999,
|
|
elevation: 6,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.2,
|
|
shadowRadius: 4,
|
|
minHeight: Spacing.touchTarget,
|
|
justifyContent: 'center',
|
|
},
|
|
text: {
|
|
color: Colors.white,
|
|
fontSize: 15,
|
|
fontWeight: '600',
|
|
textAlign: 'center',
|
|
},
|
|
});
|