Files
calorie-counter/mobile/src/components/GoalBanner.tsx
Andris Enins 12820632e7
Some checks failed
CI / Build & test backend (push) Failing after 14m56s
feat: Phase 4 — 9 new features (v1.1)
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
2026-05-19 02:11:23 +03:00

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',
},
});