feat: initial implementation — all 35 requirements across phases 1-3
Backend (Spring Boot 3.2 / Java 21 / PostgreSQL): - JWT auth with BCrypt password hashing - User profile + Mifflin-St Jeor BMR calculator - Food search + barcode via OpenFoodFacts API with local cache - Meal CRUD with user data isolation and ownership checks - AI photo analysis (OpenAI Vision) with confidence intervals - AI correction feedback loop for personalisation - Flyway DB migrations + RFC-7807 error responses Mobile (React Native / TypeScript): - Full navigation stack (Auth → Tabs → Home stack) - Design tokens (WCAG 2.2 AA colours, 8px grid, 48px touch targets) - 10 screens: Login, Register, Home, Search, Camera, AI Result, Edit Meal, Daily Details, History, Profile - Confidence-aware calorie display (kcal ± range) - Repeat last meal shortcut + macro tracking Docs: - docs/PLAN-AND-REQUIREMENTS.md - docs/traceability.csv (35 requirements, all Implemented)
This commit is contained in:
114
mobile/src/services/api.ts
Normal file
114
mobile/src/services/api.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
// Generated by GitHub Copilot
|
||||
import axios from 'axios';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
const BASE_URL = process.env.API_BASE_URL ?? 'http://localhost:8080';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: 15_000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
// Attach JWT to every request
|
||||
api.interceptors.request.use(async config => {
|
||||
const token = await AsyncStorage.getItem('jwt_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Types
|
||||
export interface FoodItem {
|
||||
id: string;
|
||||
name: string;
|
||||
source: string;
|
||||
barcode?: string;
|
||||
caloriesPer100g: number;
|
||||
proteinG?: number;
|
||||
fatG?: number;
|
||||
carbsG?: number;
|
||||
}
|
||||
|
||||
export interface MealItem {
|
||||
id: string;
|
||||
foodItem: FoodItem;
|
||||
quantityGrams: number;
|
||||
calories: number;
|
||||
}
|
||||
|
||||
export interface MealEntry {
|
||||
id: string;
|
||||
date: string;
|
||||
mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack';
|
||||
source: 'manual' | 'barcode' | 'photo';
|
||||
confidence?: number;
|
||||
items: MealItem[];
|
||||
totalCalories: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface DailyOverview {
|
||||
date: string;
|
||||
totalCalories: number;
|
||||
target: number;
|
||||
remaining: number;
|
||||
meals: MealEntry[];
|
||||
}
|
||||
|
||||
export interface AiSuggestion {
|
||||
name: string;
|
||||
grams: number;
|
||||
confidence: number;
|
||||
estimatedCalories: number;
|
||||
confidenceLow: number;
|
||||
confidenceHigh: number;
|
||||
}
|
||||
|
||||
export interface AiAnalysisResponse {
|
||||
analysisId: string;
|
||||
suggestions: AiSuggestion[];
|
||||
}
|
||||
|
||||
// Auth
|
||||
export const register = (email: string, password: string) =>
|
||||
api.post<{ userId: string; token: string }>('/auth/register', { email, password });
|
||||
|
||||
export const login = (email: string, password: string) =>
|
||||
api.post<{ userId: string; token: string }>('/auth/login', { email, password });
|
||||
|
||||
// User
|
||||
export const getProfile = () => api.get('/user/profile');
|
||||
export const updateProfile = (data: object) => api.put('/user/profile', data);
|
||||
|
||||
// Food
|
||||
export const searchFoods = (query: string) =>
|
||||
api.get<FoodItem[]>('/foods', { params: { query } });
|
||||
|
||||
export const getFoodByBarcode = (code: string) =>
|
||||
api.get<FoodItem>(`/foods/barcode/${encodeURIComponent(code)}`);
|
||||
|
||||
// Meals
|
||||
export const getDailyOverview = (date: string) =>
|
||||
api.get<DailyOverview>('/meals/daily', { params: { date } });
|
||||
|
||||
export const getMealHistory = (from: string, to: string) =>
|
||||
api.get<MealEntry[]>('/meals/history', { params: { from, to } });
|
||||
|
||||
export const createMeal = (payload: object) =>
|
||||
api.post<MealEntry>('/meals', payload);
|
||||
|
||||
export const deleteMeal = (id: string) =>
|
||||
api.delete(`/meals/${encodeURIComponent(id)}`);
|
||||
|
||||
// AI
|
||||
export const analyzeMealPhoto = (imageFormData: FormData) =>
|
||||
api.post<AiAnalysisResponse>('/ai/analyze-meal', imageFormData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
|
||||
export const saveAiCorrections = (analysisId: string, corrections: { name: string; correctedGrams: number }[]) =>
|
||||
api.post('/ai/correction', { analysisId, corrections });
|
||||
|
||||
export default api;
|
||||
Reference in New Issue
Block a user