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:
2026-05-18 21:56:13 +03:00
commit 91cd18aec6
106 changed files with 13886 additions and 0 deletions

114
mobile/src/services/api.ts Normal file
View 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;