feat: Phase 4 — 9 new features (v1.1)
Some checks failed
CI / Build & test backend (push) Failing after 14m56s
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
This commit is contained in:
90
mobile/src/components/GoalBanner.tsx
Normal file
90
mobile/src/components/GoalBanner.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
// 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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user