Compare commits

..

17 Commits

Author SHA1 Message Date
Louison PARANT dfc3b9b848 Code Smell Fix
continuous-integration/drone/push Build is passing Details
1 year ago
Louison PARANT 3ff45314bc Merge branch 'master' of https://codefirst.iut.uca.fr/git/Sae_LeftOvers/LeftOvers
continuous-integration/drone/push Build is passing Details
1 year ago
Louison PARANT a7ace455b0 Correction push
1 year ago
Rémi REGNAULT 8acd086038 Merge branch 'master' of https://codefirst.iut.uca.fr/git/Sae_LeftOvers/LeftOvers
continuous-integration/drone/push Build is passing Details
1 year ago
Rémi REGNAULT 895ff757d4 feat: recipe suggestion, when filters are selected, we use the ia filters request
continuous-integration/drone/push Build is passing Details
1 year ago
Louison PARANT 62d79c5d64 Code Smells Fix
continuous-integration/drone/push Build is passing Details
1 year ago
Rémi REGNAULT 07600429e4 Merge branch 'master' into WORK-RRE
1 year ago
Louison PARANT 18232207bd Merge branch 'master' into WORK-LPA
continuous-integration/drone/push Build is passing Details
1 year ago
Louison PARANT 31e7ed6bda Diets Management
continuous-integration/drone/push Build is passing Details
1 year ago
Rémi REGNAULT 75ee79b7c8 feat: add function to recipe service for filters
1 year ago
Rayhân HASSOU d34336ed25 Merge pull request 'add more dictionnary' (#25) from WORK-RHA2 into master
continuous-integration/drone/push Build is passing Details
1 year ago
Rémi REGNAULT d4cd47e9b2 Merge branch 'master' into WORK-RRE
1 year ago
Rémi REGNAULT 4b4a113ba9 merge master
continuous-integration/drone/push Build is passing Details
1 year ago
Rémi REGNAULT cb483782cd tests: add tests for ProfileService
continuous-integration/drone/push Build is passing Details
1 year ago
Rémi REGNAULT f124ea3d3e tests: add tests for RecipesService
1 year ago
Rémi REGNAULT a761314e58 tests: add tests for IngredientsServices
continuous-integration/drone/push Build is passing Details
1 year ago
Rémi REGNAULT 1b9746a0b5 feat: add first test for services
1 year ago

@ -0,0 +1,9 @@
export enum IngredientClass {
DairyFree = 'DAIRY_FREE',
GlutenFree = 'GLUTEN_FREE',
Porcless = 'PORCLESS',
Vegan = 'VEGAN',
Vegetarian = 'VEGETARIAN',
Pescatarian = 'PESCATARIAN',
None = 'NONE'
}

@ -1,30 +0,0 @@
export default class Profil {
private _name: string;
private _avatar: string;
private _allergy: string[];
private _diets: string[];
constructor( name: string, avatar: string, allergy: string[], diets: string[]) {
this._name = name;
this._avatar = avatar;
this._allergy = allergy;
this._diets = diets;
}
get name(): string {
return this._name;
}
get avatar(): string{
return this._avatar;
}
get allergy(): string[]{
return this._allergy;
}
get diets(): string[]{
return this._diets;
}
}

@ -4,7 +4,7 @@ import Recipes from "../../Models/Recipes";
export default class RecipesService implements IRecipesService {
private readonly API_URL = "http://leftovers.alwaysdata.net/recipes";
private readonly IA_URL = "https://codefirst.iut.uca.fr/containers/Sae_LeftOvers-leftovers_ia/getrecipes"
private readonly IA_URL = "https://codefirst.iut.uca.fr/containers/Sae_LeftOvers-leftovers_ia"
async getAllRecipes(): Promise<Recipes[]> {
try {
@ -26,9 +26,17 @@ export default class RecipesService implements IRecipesService {
}
async getRecipeWithIngredients(ids: string[]): Promise<Recipes[]>{
const recipe: Recipes[] = [];
try {
const response = await axios.get(`${this.IA_URL}/${ids}`);
const response = await axios.get(`${this.IA_URL}/getrecipes/${ids}`);
return response.data as Recipes[];
} catch (error) {
throw new Error('Erreur lors de la récupération des recettes dans getRecipeWithIngredients : ' + error.message);
}
}
async getRecipeWithIngredientsAndFilters(ids: string[], filters: string): Promise<Recipes[]> {
try {
const response = await axios.get(`${this.IA_URL}/getrecipeswithfilters/${ids}/${filters}`);
return response.data as Recipes[];
} catch (error) {
throw new Error('Erreur lors de la récupération des recettes dans getRecipeWithIngredients : ' + error.message);

@ -0,0 +1,38 @@
import IngredientService from '../Services/Ingredients/IngredientsServices';
describe('IngredientService', () => {
const ingredient_service = new IngredientService();
it('should get one ingredient', async () => {
const result = await ingredient_service.getIngredientById(1)
expect(result.id).toBe(1);
});
it('should get all ingredients', async () => {
const result = await ingredient_service.getAllIngredient()
const test = result.length >= 1
expect(test).toBe(true);
});
it('should return several ingredients starting by letter a', async () => {
const result = await ingredient_service.getIngredientByLetter('a')
let test = true
for (let ingredient of result) {
if (ingredient.name[0] !== 'a') {
test = false
}
}
expect(test).toBe(true);
});
it('should return several ingredients with car in the name', async () => {
const result = await ingredient_service.getfilteredIngredient('car')
let test = true
for (let ingredient of result) {
if (!ingredient.name.includes('car')) {
test = false
}
}
expect(test).toBe(true);
});
});

@ -0,0 +1,124 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import ProfileService from '../Services/Profiles/ProfileService';
import Profile from '../Models/Profile';
type AsyncStorageMock = {
getItem: jest.Mock<Promise<string | null>, [string]>,
setItem: jest.Mock<Promise<void>, [string, string]>,
clear: jest.Mock<Promise<void>>,
};
jest.mock('@react-native-async-storage/async-storage', () => {
const asyncStorageMock: AsyncStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
clear: jest.fn(),
};
return asyncStorageMock;
});
describe('ProfileService', () => {
beforeEach(() => {
(AsyncStorage.getItem as jest.Mock).mockReset();
(AsyncStorage.setItem as jest.Mock).mockReset();
(AsyncStorage.clear as jest.Mock).mockReset();
});
describe('getProfiles', () => {
it('should return an empty array if no profiles are stored', async () => {
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(null);
const profileService = new ProfileService();
const profiles = await profileService.getProfiles();
expect(profiles).toEqual([]);
});
it('should return an array of profiles if profiles are stored', async () => {
const storedProfiles = [
{ _name: 'John', _avatar: 'avatar1', _allergy: ['none'], _diets: [] },
{ _name: 'Jane', _avatar: 'avatar2', _allergy: ['peanuts'], _diets: ['vegan'] },
];
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(JSON.stringify(storedProfiles));
const profileService = new ProfileService();
const profiles = await profileService.getProfiles();
expect(profiles.length).toBe(2);
expect(profiles[0]).toBeInstanceOf(Profile);
expect(profiles[0].name).toEqual('John');
});
});
describe('addProfile', () => {
it('should add a new profile to the stored profiles', async () => {
const existingProfiles = [
{ _name: 'John', _avatar: 'avatar1', _allergy: ['none'], _diets: [] },
];
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(JSON.stringify(existingProfiles));
(AsyncStorage.setItem as jest.Mock).mockResolvedValueOnce(null);
const newProfile = new Profile('Jane', 'avatar2', ['peanuts'], ['vegan']);
const profileService = new ProfileService();
const result = await profileService.addProfile(newProfile);
expect(result).toBe(true);
expect(AsyncStorage.setItem).toHaveBeenCalledWith('profiles', expect.any(String));
});
it('should not add a profile if it already exists', async () => {
const existingProfiles = [
{ _name: 'John', _avatar: 'avatar1', _allergy: ['none'], _diets: [] },
];
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(JSON.stringify(existingProfiles));
const existingProfile = new Profile('John', 'avatar1', ['none'], []);
const profileService = new ProfileService();
const result = await profileService.addProfile(existingProfile);
expect(result).toBe(false);
expect(AsyncStorage.setItem).not.toHaveBeenCalled();
});
});
describe('delProfile', () => {
it('should delete a profile by name', async () => {
const existingProfiles = [
{ _name: 'John', _avatar: 'avatar1', _allergy: ['none'], _diets: [] },
{ _name: 'Jane', _avatar: 'avatar2', _allergy: ['peanuts'], _diets: ['vegan'] },
];
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(JSON.stringify(existingProfiles));
(AsyncStorage.setItem as jest.Mock).mockResolvedValueOnce(null);
const profileService = new ProfileService();
const result = await profileService.delProfile('John');
expect(result).toBe(true);
expect(AsyncStorage.setItem).toHaveBeenCalledWith('profiles', expect.any(String));
});
it('should return false if the profile does not exist', async () => {
const existingProfiles = [
{ _name: 'John', _avatar: 'avatar1', _allergy: ['none'], _diets: [] },
];
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(JSON.stringify(existingProfiles));
const profileService = new ProfileService();
const result = await profileService.delProfile('Jane');
expect(result).toBe(false);
expect(AsyncStorage.setItem).not.toHaveBeenCalled();
});
});
});

@ -0,0 +1,22 @@
import RecipesService from '../Services/Recipes/RecipesServices';
describe('RecipesService', () => {
const recipe_service = new RecipesService();
it('should get one recipe', async () => {
const result = await recipe_service.getRecipeById(4444)
expect(result.id).toBe(4444);
});
it('should get all recipes', async () => {
const result = await recipe_service.getAllRecipes()
const test = result.length >= 1
expect(test).toBe(true);
}, 120000);
it('should get one recipe', async () => {
const result = await recipe_service.getRecipeWithIngredients(['1928:2148:2809:2853:3723:6261:6335:7076'])
const test = result.length >= 1
expect(test).toBe(true);
});
});

@ -1,5 +1,5 @@
import React, {useContext, useState} from 'react';
import {StyleSheet, Pressable, Text, View, Image, ScrollView, ImageSourcePropType} from 'react-native';
import React, {useContext} from 'react';
import {StyleSheet, Pressable, Text, View, Image, ScrollView} from 'react-native';
import brochette from '../assets/images/brochette.png';
import Union_left from '../assets/images/Union_left.png';
import Union_right from '../assets/images/Union_right.png';

File diff suppressed because it is too large Load Diff

@ -6,7 +6,8 @@
"start": "expo start --dev-client",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
"web": "expo start --web",
"test": "jest --coverage"
},
"dependencies": {
"@expo/webpack-config": "^19.0.0",
@ -18,6 +19,7 @@
"axios": "^1.6.2",
"expo": "~49.0.15",
"expo-blur": "^12.4.1",
"expo-image-picker": "~14.3.2",
"expo-linear-gradient": "~12.3.0",
"expo-splash-screen": "~0.20.5",
"expo-status-bar": "~1.6.0",
@ -31,11 +33,12 @@
"react-native-splash-screen": "^3.3.0",
"react-native-virtualized-view": "^1.0.0",
"react-native-web": "~0.19.6",
"typescript": "^5.1.3",
"expo-image-picker": "~14.3.2"
"typescript": "^5.1.3"
},
"devDependencies": {
"@babel/core": "^7.20.0"
"@babel/core": "^7.20.0",
"@types/jest": "^29.5.11",
"jest": "^29.7.0"
},
"private": true
}

@ -10,6 +10,7 @@ import ColorContext from '../theme/ColorContext';
import eventEmitter from './EventEmitter';
import AsyncStorage from '@react-native-async-storage/async-storage';
import ProfileService from '../Services/Profiles/ProfileService';
import { IngredientClass } from '../Models/IngredientClass';
export default function FiltersSelection(props) {
const {colors} = useContext(ColorContext);
@ -24,6 +25,7 @@ export default function FiltersSelection(props) {
const [dieAdd, setDieAdd] = useState([])
const [allAdd, setAllAdd] = useState([])
const [selectedDiets, setSelectedDiets] = useState([])
const [activeDiets, setActiveDiets] = useState([])
const fetchProfiles = async () => {
setProfiles(await profileService.getProfiles())
@ -234,7 +236,65 @@ export default function FiltersSelection(props) {
},
});
const goBack = () => props.navigation.goBack();
const handleSaveFilters = async () => {
let dieTemp = []
let retType = true
profiles.forEach((profile) => {
if(profile.isActive == "flex"){
profile.diets.forEach((diet) => {
retType = true
dieTemp.forEach((val) => {
if(val == diet){
retType = false
}
})
if(retType){
dieTemp.push(diet)
}
})
}
})
selectedDiets.forEach((diet) => {
retType = true
dieTemp.forEach((val) => {
if(val == diet){
retType = false
}
})
if(retType){
dieTemp.push(diet)
}
})
setActiveDiets(await handleCastFilters(dieTemp))
eventEmitter.emit("updateActiveDiets")
props.navigation.goBack()
}
const handleCastFilters = async (values) => {
let diets = []
values.forEach((val) => {
if (val == "Dairy free"){
diets.push(IngredientClass.DairyFree)
}
else if (val == "Gluten free"){
diets.push(IngredientClass.GlutenFree)
}
else if (val == "Porkless"){
diets.push(IngredientClass.Porcless)
}
else if (val == "Vegan"){
diets.push(IngredientClass.Vegan)
}
else if (val == "Vegetarian"){
diets.push(IngredientClass.Vegetarian)
}
else{
diets.push(IngredientClass.Pescatarian)
}
})
await AsyncStorage.setItem('activeDiets', JSON.stringify(diets));
return diets
}
return (
<SafeAreaProvider style={{flex: 1}}>
@ -273,7 +333,7 @@ export default function FiltersSelection(props) {
<ValidateButton title="Add Allergy" image="plus.png" colour={colors.buttonDetail} backColour={colors.buttonBackground} todo={() => props.navigation.navigate("IngredientSelection")}></ValidateButton>
</View>
<View style={{marginTop: "6%"}}/>
<ValidateButton title="Save Filters" image="save.png" colour={colors.buttonMain} backColour={colors.cardBackground} todo={goBack}></ValidateButton>
<ValidateButton title="Save Filters" image="save.png" colour={colors.buttonMain} backColour={colors.cardBackground} todo={handleSaveFilters}></ValidateButton>
<View style={{marginTop: "20%"}}/>
</LinearGradient>
</ScrollView>

@ -17,7 +17,8 @@ import plus from '../assets/images/plus_small.png';
import minus from '../assets/images/minus.png';
import RecipesServices from '../Services/Recipes/RecipesServices';
import Recipes from '../Models/Recipes';
import eventEmitter from './EventEmitter';
import AsyncStorage from '@react-native-async-storage/async-storage';
export default function RecipeSuggestion({ route, navigation }) {
const {colors} = useContext(ColorContext)
@ -32,10 +33,10 @@ export default function RecipeSuggestion({ route, navigation }) {
const recipeService = new RecipesServices();
const {ingredients} = route.params;
const limitedList = ingredients.slice(minCpt, maxCpt);
const [activeDiets, setActiveDiets] = useState([])
let selectedIngredients: string[];
const die = [{value: "Gluten free"}, {value: "Porkless"}, {value: "Gluten free"}, {value: "Porkless"}]
const all = []
@ -92,21 +93,34 @@ export default function RecipeSuggestion({ route, navigation }) {
const loadRecipes = async () => {
const ids: string[] = getIngredientsIds(ingredients);
console.log('load recipes - active diets tab :', activeDiets)
const filters: string = activeDiets.map(diet => diet).join(':')
try {
const recipes: Recipes[] = await recipeService.getRecipeWithIngredients(ids);
if(recipes[0].id != -1 ){
setSelectedRecipes(recipes);
if (filters) {
const recipes: Recipes[] = await recipeService.getRecipeWithIngredientsAndFilters(ids, filters)
if(recipes[0].id != -1 ){
setSelectedRecipes(recipes);
}
}
else {
const recipes: Recipes[] = await recipeService.getRecipeWithIngredients(ids);
if(recipes[0].id != -1 ){
setSelectedRecipes(recipes);
}
}
} catch (error) {
console.error(error)
}
};
useEffect(() => {
loadRecipes();
fetchActiveDiets()
}, []);
useEffect(() => {
loadRecipes();
}, [activeDiets]);
const styles = StyleSheet.create({
linearGradient: {
width: "100%",
@ -196,6 +210,23 @@ export default function RecipeSuggestion({ route, navigation }) {
},
});
const subscriptionUpdateActiveDiets = eventEmitter.addListener('updateActiveDiets', async () => {
fetchActiveDiets()
subscriptionUpdateActiveDiets.remove();
});
const fetchActiveDiets = async () => {
try {
const results = await AsyncStorage.getItem('activeDiets');
let existingActiveDiets = JSON.parse(results);
if (existingActiveDiets.length === 0) {
existingActiveDiets = [];
}
setActiveDiets(existingActiveDiets)
} catch (error) {
console.error('Error fetching active diets:', error);
}
}
const ingredientElements = limitedList.map((source, index) => (
<View style={[styles.horizontalAlignment, {marginVertical: "3%"}]} key={index}>
@ -270,9 +301,9 @@ export default function RecipeSuggestion({ route, navigation }) {
<View style={styles.background}>
<View style={styles.filterBar}>
<Text style={styles.filters}>Additional Filters</Text>
<Text style={styles.nbSelected}>{die.length} selected</Text>
<Text style={styles.nbSelected}>{activeDiets.length} selected</Text>
</View>
<ListWithoutSelect title="Diets" content={die}></ListWithoutSelect>
<ListWithoutSelect title="Diets" content={activeDiets}></ListWithoutSelect>
<View style={{marginTop: "3%"}}/>
<ListWithoutSelect title="Allergies" content={all}></ListWithoutSelect>
<View style={{marginTop: "3%"}}/>

8723
package-lock.json generated

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save