Compare commits

..

No commits in common. 'master' and 'part6' have entirely different histories.

@ -1,17 +0,0 @@
kind: pipeline
type: docker
name: JokesApp
steps:
- name: sonar-analyses
image: hub.codefirst.iut.uca.fr/camille.petitalot/drone-sonarplugin-reactnative:latest
commands:
- cd src
- npm install
- sonar-scanner -Dsonar.projectKey=JokesAppReact_A-PE -Dsonar.sources=. -Dsonar.host.url=$${PLUGIN_SONAR_HOST}
-Dsonar.login=$${PLUGIN_SONAR_TOKEN}
secrets: [ SECRET_SONAR_LOGIN ]
settings:
sonar_host: https://codefirst.iut.uca.fr/sonar/
sonar_token:
from_secret: SECRET_SONAR_LOGIN

@ -64,11 +64,11 @@ Le projet de tp utilise un modèle de flux de travail Git pour organiser le dév
* [X] Partie 4 - Components * [X] Partie 4 - Components
* [X] Partie 5 - FlatList * [X] Partie 5 - FlatList
* [X] Partie 6 - Safe Area * [X] Partie 6 - Safe Area
* [X] Partie 7 - Navigation * [ ] Partie 7 - Navigation
* [X] Partie 8 - Hooks * [ ] Partie 8 - Hooks
* [X] Partie 9 - Redux Store * [ ] Partie 9 - Redux Store
* [X] Partie 10 - Async Storage * [ ] Partie 10 - Async Storage
* [X] Partie 11 - Theming * [ ] Partie 11 - Theming
* [ ] Partie 12 - Unit testing * [ ] Partie 12 - Unit testing
* [ ] Partie 13 - Resources * [ ] Partie 13 - Resources

@ -1,32 +1,5 @@
import {Theme} from "@react-navigation/native";
export const indigoColor = "rgba(14, 14, 44, 1)"; export const indigoColor = "rgba(14, 14, 44, 1)";
export const purpleColor = "rgba(74, 74, 104, 1)"; export const purpleColor = "rgba(74, 74, 104, 1)";
export const darksalmonColor = "rgba(233, 150, 122, 1)"; export const darksalmonColor = "rgba(233, 150, 122, 1)";
export const greyColor = "rgba(140, 140, 161, 1)"; export const greyColor = "rgba(140, 140, 161, 1)";
export const whiteColor = "rgba(239, 239, 253, 1)"; export const whiteColor = "rgba(239, 239, 253, 1)";
export const LightTheme: Theme = {
dark: false,
colors: {
primary: darksalmonColor,
background: whiteColor,
card: greyColor,
text: "black",
border: whiteColor,
notification: 'rgb(255, 59, 48)',
},
};
export const DarkTheme: Theme = {
dark: true,
colors: {
primary: darksalmonColor,
background: purpleColor,
card: indigoColor,
text: whiteColor,
border: greyColor,
notification: 'rgb(255, 69, 58)',
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 B

@ -1,26 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import Categ from './Categ';
import { Text, View } from 'react-native';
import {describe, expect, it} from "@jest/globals";
describe('Categ Component', () => {
it('renders correctly', () => {
const category = { name: 'Test Category' };
const wrapper = shallow(<Categ category={category} />);
expect(wrapper).toMatchSnapshot();
});
it('displays category name', () => {
const category = { name: 'Test Category' };
const wrapper = shallow(<Categ category={category} />);
expect(wrapper.find(Text).prop('children')).toEqual(category.name);
});
it('applies correct styles', () => {
const category = { name: 'Test Category' };
const wrapper = shallow(<Categ category={category} />);
const containerStyle = wrapper.find(View).prop('style');
expect(containerStyle).toEqual(expect.objectContaining(styles.bottomContainer));
});
});

@ -1,36 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import Categs from './Categs';
import { FlatList } from 'react-native';
import Categ from './Categ';
import { Category } from '../model/Category';
import {describe, expect, it} from "@jest/globals";
describe('Categs Component', () => {
const categories: Category[] = [
new Category('Category 1', 10),
new Category('Category 2', 5),
new Category('Category 3', 15),
];
it('renders correctly', () => {
const wrapper = shallow(<Categs categories={categories} />);
expect(wrapper).toMatchSnapshot();
});
it('renders FlatList with correct props', () => {
const wrapper = shallow(<Categs categories={categories} />);
const flatList = wrapper.find(FlatList);
expect(flatList.exists()).toBeTruthy();
expect(flatList.prop('showsHorizontalScrollIndicator')).toBe(false);
expect(flatList.prop('horizontal')).toBe(true);
expect(flatList.prop('data')).toEqual(categories.sort((a, b) => b.number - a.number));
expect(flatList.prop('keyExtractor')).toBeInstanceOf(Function);
expect(flatList.prop('renderItem')).toBeInstanceOf(Function);
});
it('renders correct number of Categ components', () => {
const wrapper = shallow(<Categs categories={categories} />);
expect(wrapper.find(Categ)).toHaveLength(categories.length);
});
});

@ -1,29 +1,16 @@
import {StyleSheet, Text, View, Image, Button, TouchableOpacity} from 'react-native'; import {StyleSheet, Text, View, Image, Button, TouchableOpacity} from 'react-native';
import {SampleJoke} from "../model/SampleJoke"; import {SampleJoke} from "../model/SampleJoke";
import {darksalmonColor, whiteColor, greyColor, indigoColor, purpleColor} from "../assets/Theme"; import {darksalmonColor, whiteColor, greyColor, indigoColor, purpleColor} from "../assets/Theme";
import React, {useEffect, useState} from "react"; import React, {useState} from "react";
import {CustomJoke} from "../model/CustomJoke"; import {CustomJoke} from "../model/CustomJoke";
import {useDispatch, useSelector} from "react-redux";
import {getCustomJokeById, getJokeById} from "../redux/thunk/GetByThunk";
import {getCustomJokesList} from "../redux/thunk/GetThunk";
import {deleteCustomJoke} from "../redux/thunk/DeleteThunk";
import {useNavigation} from "@react-navigation/native";
import {AppDispatch} from "../redux/store";
type JokeItemProps = { type JokeItemProps = {
joke: SampleJoke | CustomJoke; joke: SampleJoke | CustomJoke;
}; };
const eye = require("../assets/eye_icon.png")
const hideEye = require("../assets/eye_off_icon.png")
const heart = require("../assets/favorite_icon.png")
const bin = require("../assets/bin.png")
const heartPlain = require("../assets/plain_favorite_icon.png")
export default function JokeDetail(props: JokeItemProps) { export default function JokeDetail(props: JokeItemProps) {
const [showDescription, setShowDescription] = useState(false); const [showDescription, setShowDescription] = useState(false); // état local pour contrôler la visibilité de la description
const [showFavorite, setShowFavorite] = useState(false); const [showFavorite, setShowFavorite] = useState(false); // état local pour contrôler la visibilité du favori
const isCustom = props.joke instanceof CustomJoke;
const dispatch = useDispatch<AppDispatch>();
const navigation = useNavigation();
const toggleDescription = () => { const toggleDescription = () => {
setShowDescription(!showDescription); setShowDescription(!showDescription);
@ -33,11 +20,10 @@ export default function JokeDetail(props: JokeItemProps) {
setShowFavorite(!showFavorite); setShowFavorite(!showFavorite);
}; };
const deleteCustomJokes = async () => { const eye = require("../assets/eye_icon.png")
await dispatch(deleteCustomJoke(props.joke.id)); const hideEye = require("../assets/eye_off_icon.png")
await dispatch(getCustomJokesList()); const heart = require("../assets/favorite_icon.png")
navigation.goBack(); const heartPlain = require("../assets/plain_favorite_icon.png")
};
return( return(
<View style={styles.container}> <View style={styles.container}>
@ -47,15 +33,6 @@ export default function JokeDetail(props: JokeItemProps) {
</View> </View>
<Text style={styles.text}>Résumé de la blague</Text> <Text style={styles.text}>Résumé de la blague</Text>
<View style={styles.row}> <View style={styles.row}>
{isCustom && (<TouchableOpacity style={styles.favContainer} onPress={ deleteCustomJokes }>
<View>
<Image
source={bin}
style={styles.imageButton}
/>
</View>
</TouchableOpacity>)
}
<TouchableOpacity style={styles.favContainer} onPress={toggleFavorite}> <TouchableOpacity style={styles.favContainer} onPress={toggleFavorite}>
<View> <View>
<Image <Image
@ -172,7 +149,7 @@ const styles = StyleSheet.create({
marginLeft: 19, marginLeft: 19,
marginTop: 20, marginTop: 20,
borderRadius: 20, borderRadius: 20,
width : 150, width : 120,
alignItems : 'center' alignItems : 'center'
} }
}) })

@ -1,54 +0,0 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import JokeDetail from '../path/to/JokeDetail';
import {describe} from "@jest/globals"; // Remplacez le chemin par le bon chemin
describe('JokeDetail Component', () => {
const sampleJoke = {
id: '1',
type: 'Sample',
image: 'sample-image-url',
description: () => 'Sample Joke Description'
};
const customJoke = {
id: '2',
type: 'Custom',
image: 'custom-image-url',
description: () => 'Custom Joke Description'
};
test('renders joke image', () => {
const { getByTestId } = render(<JokeDetail joke={sampleJoke} />);
const jokeImage = getByTestId('joke-image');
expect(jokeImage).toBeTruthy();
});
test('renders joke description when toggled', () => {
const { getByText } = render(<JokeDetail joke={sampleJoke} />);
const toggleButton = getByText('LA CHUTE');
fireEvent.press(toggleButton);
const jokeDescription = getByText('Sample Joke Description');
expect(jokeDescription).toBeTruthy();
});
test('toggles favorite icon when pressed', () => {
const { getByTestId } = render(<JokeDetail joke={sampleJoke} />);
const favoriteButton = getByTestId('favorite-button');
fireEvent.press(favoriteButton);
expect(favoriteButton.props.source).toEqual(require('../assets/plain_favorite_icon.png'));
});
test('calls deleteCustomJokes and updates list when delete button is pressed for custom joke', () => {
const deleteMock = jest.fn();
const dispatchMock = jest.fn();
jest.mock('../redux/thunk/DeleteThunk', () => ({ deleteCustomJoke: deleteMock }));
jest.mock('react-redux', () => ({ useDispatch: () => dispatchMock }));
const { getByTestId } = render(<JokeDetail joke={customJoke} />);
const deleteButton = getByTestId('delete-button');
fireEvent.press(deleteButton);
expect(deleteMock).toHaveBeenCalledWith(customJoke.id);
expect(dispatchMock).toHaveBeenCalledTimes(2); // Assuming one dispatch for delete and one for update list
});
});

@ -1,25 +0,0 @@
import { Category } from './Category';
import {describe, expect, it} from "@jest/globals";
describe('Category Class Constructor', () => {
it('should create a new Category object', () => {
const category = new Category('name', 5);
expect(category).toBeDefined();
expect(category.name).toBe('name');
expect(category.number).toBe(5);
});
});
describe('Category Class Accessors', () => {
it('should set and get the name correctly', () => {
const category = new Category('name', 5);
category.name = 'newName';
expect(category.name).toBe('newName');
});
it('should set and get the number correctly', () => {
const category = new Category('name', 5);
category.number = 10;
expect(category.number).toBe(10);
});
});

@ -1,35 +0,0 @@
import { CustomJoke } from './CustomJoke';
import {describe, expect, test} from "@jest/globals"; // Remplacez le chemin par le bon chemin
describe('CustomJoke Class', () => {
const id = '1';
const type = 'Custom';
const setup = 'Why did the developer go broke?';
const image = 'custom-image-url';
const punchline = 'Because he used up all his cache';
test('creates a new instance of CustomJoke', () => {
const customJoke = new CustomJoke(id, type, setup, image, punchline);
expect(customJoke).toBeInstanceOf(CustomJoke);
expect(customJoke.id).toEqual(id);
expect(customJoke.type).toEqual(type);
expect(customJoke.setup).toEqual(setup);
expect(customJoke.punchline).toEqual(punchline);
expect(customJoke.image).toEqual(image);
});
test('updates CustomJoke properties', () => {
const newSetup = 'Why do programmers prefer dark mode?';
const newImage = 'new-custom-image-url';
const newPunchline = 'Because light attracts bugs';
const customJoke = new CustomJoke(id, type, setup, image, punchline);
customJoke.setup = newSetup;
customJoke.image = newImage;
customJoke.punchline = newPunchline;
expect(customJoke.setup).toEqual(newSetup);
expect(customJoke.image).toEqual(newImage);
expect(customJoke.punchline).toEqual(newPunchline);
});
});

@ -1,61 +0,0 @@
const { Joke } = require('./Joke');
const {expect, it, beforeEach, describe} = require("@jest/globals");
// Mock class extending the abstract Joke class
class MockJoke extends Joke {
constructor(type, setup, punchline, image) {
super(type, setup, punchline, image);
}
}
// Test the Joke class
describe('Joke Class', () => {
let joke;
beforeEach(() => {
joke = new MockJoke('type', 'setup', 'punchline', 'image');
});
// Test the constructor
it('should create a new Joke object', () => {
expect(joke).toBeDefined();
expect(joke.type).toBe('type');
expect(joke.setup).toBe('setup');
expect(joke.punchline).toBe('punchline');
expect(joke.image).toBe('image');
});
// Test the summary() method
it('should return a summary of the joke', () => {
expect(joke.summary()).toBe('punchline');
});
// Test the description() method
it('should return a textual description of the joke', () => {
expect(joke.description()).toBe('type, punchline');
});
// Test setting and getting the type
it('should set and get the type correctly', () => {
joke.type = 'newType';
expect(joke.type).toBe('newType');
});
// Test setting and getting the setup
it('should set and get the setup correctly', () => {
joke.setup = 'newSetup';
expect(joke.setup).toBe('newSetup');
});
// Test setting and getting the punchline
it('should set and get the punchline correctly', () => {
joke.punchline = 'newPunchline';
expect(joke.punchline).toBe('newPunchline');
});
// Test setting and getting the image
it('should set and get the image correctly', () => {
joke.image = 'newImage';
expect(joke.image).toBe('newImage');
});
});

@ -1,4 +1,4 @@
import {DarkTheme, DefaultTheme, NavigationContainer, Theme} from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import {Image, View} from 'react-native'; import {Image, View} from 'react-native';
@ -9,8 +9,6 @@ import Settings from "../screens/Settings";
import {darksalmonColor, greyColor, indigoColor} from "../assets/Theme"; import {darksalmonColor, greyColor, indigoColor} from "../assets/Theme";
import StackNavigation from "./StackNavigation"; import StackNavigation from "./StackNavigation";
import {useEffect, useState} from "react";
import {getTheme} from "../redux/thunk/ThemeThunk";
export default function NavigationBar() { export default function NavigationBar() {
@ -22,23 +20,8 @@ export default function NavigationBar() {
const favoriteIcon = require("../assets/favorite_icon.png"); const favoriteIcon = require("../assets/favorite_icon.png");
const settingsIcon = require("../assets/settings_icon.png"); const settingsIcon = require("../assets/settings_icon.png");
const [themes, setTheme] = useState<Theme>(DefaultTheme);
useEffect(() => {
const fetchTheme = async () => {
const theme = await getTheme();
setTheme(theme);
};
fetchTheme();
});
if (themes == null) {
return null;
}
return ( return (
<NavigationContainer theme={ themes.dark === false ? DefaultTheme : DarkTheme}> <NavigationContainer>
<BottomTabNavigator.Navigator initialRouteName="Accueil" <BottomTabNavigator.Navigator initialRouteName="Accueil"
screenOptions={{ screenOptions={{
headerStyle: { headerStyle: {

11987
src/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -7,57 +7,26 @@
"start": "expo start", "start": "expo start",
"android": "expo start --android", "android": "expo start --android",
"ios": "expo start --ios", "ios": "expo start --ios",
"web": "expo start --web", "web": "expo start --web"
"test": "jest"
}, },
"dependencies": { "dependencies": {
"@jest/globals": "^29.7.0",
"@react-native-async-storage/async-storage": "^1.23.1",
"@react-navigation/bottom-tabs": "^6.5.11", "@react-navigation/bottom-tabs": "^6.5.11",
"@react-navigation/native": "^6.1.10", "@react-navigation/native": "^6.1.10",
"@react-navigation/stack": "^6.3.21", "@react-navigation/stack": "^6.3.21",
"@reduxjs/toolkit": "^2.2.1", "@reduxjs/toolkit": "^2.2.1",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^12.4.5",
"@types/react": "~18.2.45", "@types/react": "~18.2.45",
"enzyme": "^3.11.0",
"expo": "~50.0.3", "expo": "~50.0.3",
"expo-status-bar": "~1.11.1", "expo-status-bar": "~1.11.1",
"jest": "^29.7.0",
"jest-expo": "^50.0.4",
"react": "18.2.0", "react": "18.2.0",
"react-native": "0.73.2", "react-native": "0.73.2",
"react-native-gesture-handler": "^2.15.0", "react-native-gesture-handler": "^2.15.0",
"react-native-safe-area-context": "^4.9.0", "react-native-safe-area-context": "^4.9.0",
"react-redux": "^9.1.0", "react-redux": "^9.1.0",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"typescript": "^5.3.0" "typescript": "^5.3.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.0", "@babel/core": "^7.20.0"
"@types/fetch-mock": "^7.3.8",
"@types/redux-mock-store": "^1.0.6",
"fetch-mock": "^9.11.0",
"redux-mock-store": "^1.5.4"
},
"jest": {
"preset": "jest-expo",
"verbose": true,
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)"
],
"testMatch": [
"**.test.js"
],
"testEnvironment": "node",
"testEnvironmentOptions": {
"browsers": [
"chrome",
"firefox",
"safari"
]
}
}, },
"private": true "private": true
} }

@ -1,23 +0,0 @@
import { ActionType, Action, setCategoriesList } from './CategoryAction';
import { Category } from '../../model/Category';
import {describe, expect, it} from "@jest/globals";
describe('Actions', () => {
describe('setCategoriesList', () => {
it('should create an action to set categories list', () => {
const categoriesList: Category[] = [
new Category('Category 1', 1),
new Category('Category 2', 2)
];
const expectedAction: Action = {
type: ActionType.FETCH_CATEGORIES,
payload: categoriesList,
};
const action = setCategoriesList(categoriesList);
expect(action).toEqual(expectedAction);
});
});
});

@ -4,8 +4,7 @@ import {SampleJoke} from "../../model/SampleJoke";
export enum ActionType { export enum ActionType {
FETCH_CUSTOM_JOKES = 'FETCH_CUSTOM_JOKES', FETCH_CUSTOM_JOKES = 'FETCH_CUSTOM_JOKES',
POST_CUSTOM_JOKE = "POST_CUSTOM_JOKE", POST_CUSTOM_JOKE = "POST_CUSTOM_JOKE",
FETCH_JOKES_BY_ID = "FETCH_JOKES_BY_ID", FETCH_JOKES_BY_ID = "FETCH_JOKES_BY_ID"
DELETE_CUSTOM_JOKE = "DELETE_CUSTOM_JOKE"
} }
type actionPostFetch = { type actionPostFetch = {
@ -20,12 +19,7 @@ type actionGetByFetch = {
type: ActionType.FETCH_JOKES_BY_ID; type: ActionType.FETCH_JOKES_BY_ID;
payload: CustomJoke; payload: CustomJoke;
} }
type actionDeleteFetch = { export type Action = actionPostFetch | actionGetFetch | actionGetByFetch;
type: ActionType.DELETE_CUSTOM_JOKE;
payload: string;
}
export type Action = actionPostFetch | actionGetFetch | actionGetByFetch | actionDeleteFetch;
export const setPostJoke = (customJoke: CustomJoke) => { export const setPostJoke = (customJoke: CustomJoke) => {
return { return {
@ -33,6 +27,7 @@ export const setPostJoke = (customJoke: CustomJoke) => {
payload: customJoke payload: customJoke
} }
} }
export const setCustomJokesList = (customJokesList: CustomJoke[]) => { export const setCustomJokesList = (customJokesList: CustomJoke[]) => {
return { return {
type: ActionType.FETCH_CUSTOM_JOKES, type: ActionType.FETCH_CUSTOM_JOKES,
@ -45,9 +40,3 @@ export const setCustomJokeById = (customJoke: CustomJoke) => {
payload: customJoke, payload: customJoke,
}; };
} }
export const setDeleteJoke = (jokeId: string) => {
return {
type: ActionType.DELETE_CUSTOM_JOKE,
payload: jokeId
}
}

@ -1,56 +0,0 @@
import { ActionType, Action, setPostJoke, setCustomJokesList, setCustomJokeById, setDeleteJoke } from './CustomJoke';
import { CustomJoke } from '../../model/CustomJoke';
import {describe, expect, it} from "@jest/globals";
describe('Actions', () => {
describe('setPostJoke', () => {
it('should create an action to post a custom joke', () => {
const customJoke: CustomJoke = new CustomJoke('1', 'pun', 'Setup', 'https://example.com/image.jpg', 'Punchline');
const expectedAction: Action = {
type: ActionType.POST_CUSTOM_JOKE,
payload: customJoke,
};
const action = setPostJoke(customJoke);
expect(action).toEqual(expectedAction);
});
});
describe('setCustomJokesList', () => {
it('should create an action to set custom jokes list', () => {
const customJokesList: CustomJoke[] = [
new CustomJoke('1', 'pun', 'Setup 1', 'https://example.com/image1.jpg', 'Punchline 1'),
new CustomJoke('2', 'pun', 'Setup 2', 'https://example.com/image2.jpg', 'Punchline 2')
];
const expectedAction: Action = {
type: ActionType.FETCH_CUSTOM_JOKES,
payload: customJokesList,
};
const action = setCustomJokesList(customJokesList);
expect(action).toEqual(expectedAction);
});
});
describe('setCustomJokeById', () => {
it('should create an action to set custom joke by ID', () => {
const customJoke: CustomJoke = new CustomJoke('1', 'pun', 'Setup', 'https://example.com/image.jpg', 'Punchline');
const expectedAction: Action = {
type: ActionType.FETCH_JOKES_BY_ID,
payload: customJoke,
};
const action = setCustomJokeById(customJoke);
expect(action).toEqual(expectedAction);
});
});
describe('setDeleteJoke', () => {
it('should create an action to delete a custom joke', () => {
const jokeId = '1';
const expectedAction: Action = {
type: ActionType.DELETE_CUSTOM_JOKE,
payload: jokeId,
};
const action = setDeleteJoke(jokeId);
expect(action).toEqual(expectedAction);
});
});
});

@ -1,18 +0,0 @@
export enum ActionType {
SET_THEME = 'SET_THEME',
}
type actionFetch = {
type: ActionType.SET_THEME;
payload: String;
}
export type Action = actionFetch;
export const setTheme = (theme) => {
return {
type: ActionType.SET_THEME,
payload: theme,
};
}

@ -1,28 +0,0 @@
import {CustomJoke} from "../../model/CustomJoke";
import {SampleJoke} from "../../model/SampleJoke";
export enum ActionType {
ADD_FAVORITE = 'ADD_CUSTOM_FAVORITE',
REMOVE_FAVORITE = "REMOVE_CUSTOM_FAVORITE"
}
type actionAddFetch = {
type: ActionType.ADD_FAVORITE;
payload: string;
}
type actionRemoveFetch = {
type: ActionType.REMOVE_FAVORITE;
payload: string;
}
export type Action = actionAddFetch | actionRemoveFetch;
export const addFavorite = (joke: CustomJoke | SampleJoke) => ({
type: ActionType.ADD_FAVORITE,
payload: joke,
});
export const removeFavorite = (joke: CustomJoke | SampleJoke) => ({
type: ActionType.REMOVE_FAVORITE,
payload: joke,
});

@ -1,32 +0,0 @@
import { ActionType, Action, setJokesList, setJokeById } from './SampleAction';
import { SampleJoke } from '../../model/SampleJoke';
import {describe, expect, it} from "@jest/globals";
describe('Actions', () => {
describe('setJokesList', () => {
it('should create an action to set jokes list', () => {
const jokesList: SampleJoke[] = [
new SampleJoke(1, 'pun', 'Setup 1', 'https://example.com/image1.jpg', 'Punchline 1'),
new SampleJoke(2, 'pun', 'Setup 2', 'https://example.com/image2.jpg', 'Punchline 2')
];
const expectedAction: Action = {
type: ActionType.FETCH_JOKES,
payload: jokesList,
};
const action = setJokesList(jokesList);
expect(action).toEqual(expectedAction);
});
});
describe('setJokeById', () => {
it('should create an action to set joke by ID', () => {
const joke: SampleJoke = new SampleJoke(1, 'pun', 'Setup', 'https://example.com/image.jpg', 'Punchline');
const expectedAction: Action = {
type: ActionType.FETCH_JOKES_BY_ID,
payload: joke,
};
const action = setJokeById(joke);
expect(action).toEqual(expectedAction);
});
});
});

@ -1,23 +0,0 @@
import categoryReducer from './CategoryReducer';
import { ActionType as CategoryActionType } from '../actions/CategoryAction';
import { Category } from '../../model/Category';
import {describe, expect, it} from "@jest/globals";
describe('Category Reducer', () => {
it('should handle FETCH_CATEGORIES', () => {
const categories = [
new Category('Category 1', 1),
new Category('Category 2', 2)
];
const action = {
type: CategoryActionType.FETCH_CATEGORIES,
payload: categories
};
expect(categoryReducer(undefined, action)).toEqual({
categories: categories
});
});
});

@ -1,28 +1,29 @@
import {CustomJoke} from "../../model/CustomJoke"; import {CustomJoke} from "../../model/CustomJoke";
import {SampleJoke} from "../../model/SampleJoke";
import {Action, ActionType} from "../actions/CustomJoke"; import {Action, ActionType} from "../actions/CustomJoke";
import {Category} from "../../model/Category";
interface State { interface State {
customJokes: CustomJoke[]; customJokes: CustomJoke[];
customJoke?: CustomJoke; customJoke: CustomJoke;
jokeId: string;
} }
const initialState = { const initialState = {
customJokes: [], customJokes: [],
customJoke: null, customJoke: new CustomJoke('', '', '', '')
jokeId: ''
} }
const customReducer = (state: State = initialState, action: Action) => { const customReducer = (state: State = initialState, action: Action) => {
switch (action.type) { switch (action.type) {
case ActionType.POST_CUSTOM_JOKE: case ActionType.POST_CUSTOM_JOKE:
// @ts-ignore
return {...state, customJoke: action.payload}; return {...state, customJoke: action.payload};
case ActionType.FETCH_CUSTOM_JOKES: case ActionType.FETCH_CUSTOM_JOKES:
// @ts-ignore
return {...state, customJokes: action.payload}; return {...state, customJokes: action.payload};
case ActionType.FETCH_JOKES_BY_ID: case ActionType.FETCH_JOKES_BY_ID:
// @ts-ignore
return {...state, customJoke: action.payload}; return {...state, customJoke: action.payload};
case ActionType.DELETE_CUSTOM_JOKE:
return {...state, jokeId: action.payload};
default: default:
return state; return state;
} }

@ -1,29 +0,0 @@
import customReducer from './CustomReducer';
import { ActionType } from '../actions/CustomJoke';
import { CustomJoke } from '../../model/CustomJoke';
import {describe, expect, it} from "@jest/globals";
describe('Custom Reducer', () => {
it('should return the initial state', () => {
expect(customReducer(undefined, {})).toEqual({
customJokes: [],
customJoke: null,
jokeId: ''
});
});
it('should handle POST_CUSTOM_JOKE', () => {
const customJoke: CustomJoke = new CustomJoke('1', 'pun', 'Setup', 'https://example.com/image.jpg', 'Punchline');
const action = {
type: ActionType.POST_CUSTOM_JOKE,
payload: customJoke
};
expect(customReducer(undefined, action)).toEqual({
customJokes: [],
customJoke: customJoke,
jokeId: ''
});
});
});

@ -1,21 +0,0 @@
import {Action, ActionType} from "../actions/DarkModeAction";
interface State {
theme: String
}
const initialState = {
theme: 'dark'
}
const themeReducer = (state: State = initialState, action: Action) => {
switch (action.type) {
case ActionType.SET_THEME:
// @ts-ignore
return {...state, theme: action.payload};
default:
return state;
}
}
export default themeReducer;

@ -1,22 +0,0 @@
import themeReducer from './DarkModeReducer';
import { ActionType } from '../actions/DarkModeAction';
import {describe, expect, it} from "@jest/globals";
describe('Theme Reducer', () => {
// it('should return the initial state', () => {
// expect(themeReducer(undefined, {})).toEqual({
// theme: 'dark'
// });
// });
it('should handle SET_THEME', () => {
const action = {
type: ActionType.SET_THEME,
payload: 'light'
};
expect(themeReducer(undefined, action)).toEqual({
theme: 'light'
});
});
});

@ -1,26 +0,0 @@
import {Action, ActionType} from "../actions/FavoriteAction";
import {CustomJoke} from "../../model/CustomJoke";
import {SampleJoke} from "../../model/SampleJoke";
interface State {
joke: CustomJoke | SampleJoke
}
const initialState = {
joke: new CustomJoke('', '', '', '', '')
}
const favoriteReducer = (state: State = initialState, action: Action) => {
switch (action.type) {
case ActionType.ADD_FAVORITE:
// @ts-ignore
return {...state, joke: action.payload};
case ActionType.REMOVE_FAVORITE:
// @ts-ignore
return {...state, joke: action.payload};
default:
return state;
}
}
export default favoriteReducer;

@ -1,61 +0,0 @@
// Importez les réducteurs et les types d'action appropriés
import categoryReducer from './CategoryReducer';
import customReducer from './CustomReducer';
import themeReducer from './DarkModeReducer';
import sampleReducer from './SampleReducer';
import { ActionType as CategoryActionType } from '../actions/CategoryAction';
import { ActionType as CustomActionType } from '../actions/CustomJoke';
import { ActionType as ThemeActionType } from '../actions/DarkModeAction';
import { ActionType as SampleActionType } from '../actions/SampleAction';
import { Category } from '../../model/Category';
import { CustomJoke } from '../../model/CustomJoke';
import { SampleJoke } from '../../model/SampleJoke';
import {describe, expect, it} from "@jest/globals";
// Tester categoryReducer
describe('Category Reducer', () => {
it('should return the initial state', () => {
expect(categoryReducer(undefined, {})).toEqual({
categories: []
});
});
// Ajouter d'autres tests pour categoryReducer si nécessaire...
});
// Tester customReducer
describe('Custom Reducer', () => {
it('should return the initial state', () => {
expect(customReducer(undefined, {})).toEqual({
customJokes: [],
customJoke: null,
jokeId: ''
});
});
// Ajouter d'autres tests pour customReducer si nécessaire...
});
// Tester themeReducer
describe('Theme Reducer', () => {
it('should return the initial state', () => {
expect(themeReducer(undefined, {})).toEqual({
theme: 'dark'
});
});
// Ajouter d'autres tests pour themeReducer si nécessaire...
});
// Tester sampleReducer
describe('Sample Reducer', () => {
it('should return the initial state', () => {
expect(sampleReducer(undefined, {})).toEqual({
jokes: [],
favoriteJokes: [],
joke: null,
});
});
// Ajouter d'autres tests pour sampleReducer si nécessaire...
});

@ -6,13 +6,13 @@ import {Category} from "../../model/Category";
interface State { interface State {
jokes: SampleJoke[]; jokes: SampleJoke[];
favoriteJokes: SampleJoke[]; favoriteJokes: SampleJoke[];
joke? : SampleJoke; joke : SampleJoke;
} }
const initialState = { const initialState = {
jokes: [], jokes: [],
favoriteJokes: [], favoriteJokes: [],
joke:null, joke: new SampleJoke(1, "", "", "", ""),
} }
const sampleReducer = (state: State = initialState, action: Action) => { const sampleReducer = (state: State = initialState, action: Action) => {

@ -2,16 +2,12 @@ import {configureStore} from '@reduxjs/toolkit'
import sampleReducer from "./reducers/SampleReducer"; import sampleReducer from "./reducers/SampleReducer";
import categoryReducer from "./reducers/CategoryReducer"; import categoryReducer from "./reducers/CategoryReducer";
import customReducer from "./reducers/CustomReducer"; import customReducer from "./reducers/CustomReducer";
import themeReducer from "./reducers/DarkModeReducer"
import favoriteReducer from "./reducers/FavoriteReducer";
// Reference here all your application reducers // Reference here all your application reducers
const reducer = { const reducer = {
sampleReducer: sampleReducer, sampleReducer: sampleReducer,
categoryReducer: categoryReducer, categoryReducer: categoryReducer,
customReducer: customReducer, customReducer: customReducer,
theme: themeReducer,
favorite: favoriteReducer
} }
// @ts-ignore // @ts-ignore
@ -22,6 +18,5 @@ const store = configureStore({
serializableCheck: false serializableCheck: false
}) })
},); },);
export type AppState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export default store; export default store;

@ -1,29 +0,0 @@
import store from './store';
import {describe, expect, it} from "@jest/globals";
describe('Redux Store Configuration', () => {
it('should have sampleReducer', () => {
expect(store.getState().sampleReducer).toBeDefined();
});
it('should have categoryReducer', () => {
expect(store.getState().categoryReducer).toBeDefined();
});
it('should have customReducer', () => {
expect(store.getState().customReducer).toBeDefined();
});
it('should have themeReducer', () => {
expect(store.getState().theme).toBeDefined();
});
it('should have favoriteReducer', () => {
expect(store.getState().favorite).toBeDefined();
});
it('should have a configured store', () => {
expect(store).toBeDefined();
expect(store.getState()).toBeDefined();
});
});

@ -1,28 +0,0 @@
import { setDeleteJoke } from "../actions/CustomJoke";
export const deleteItem = (id: string) => {
return async dispatch => {
try {
const response = await fetch(`https://iut-weather-api.azurewebsites.net/jokes/${id}`, {
method: 'DELETE',
headers: {
Accept: "application/json",
"Content-Type": 'application/json',
}
});
if (response.ok) {
dispatch(setDeleteJoke(id)); // Supprimer la blague dans le store Redux
console.log('Suppression de la blague réussie');
} else {
console.log('Erreur lors de la requête DELETE');
}
} catch (error) {
console.log('Erreur :', error);
}
};
};
export const deleteCustomJoke = (jokeId) => {
return deleteItem(jokeId)
}

@ -1,52 +0,0 @@
import fetchMock from 'fetch-mock';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import { deleteItem } from './DeleteThunk'; // Assurez-vous d'importer correctement votre thunk
import { setDeleteJoke } from '../actions/CustomJoke';
import {afterEach, describe, expect, it, jest} from "@jest/globals";
const middlewares = [thunk];
// @ts-ignore
const mockStore = configureMockStore(middlewares);
describe('deleteItem Thunk', () => {
afterEach(() => {
fetchMock.restore();
});
it('dispatches setDeleteJoke after successful DELETE request', async () => {
const jokeId = '123';
const expectedActions = [setDeleteJoke(jokeId)];
fetchMock.deleteOnce(`https://iut-weather-api.azurewebsites.net/jokes/${jokeId}`, {
status: 200,
});
const store = mockStore({});
// @ts-ignore
await store.dispatch(deleteItem(jokeId));
expect(store.getActions()).toEqual(expectedActions);
});
it('logs an error message if DELETE request fails', async () => {
const jokeId = '123';
fetchMock.deleteOnce(`https://iut-weather-api.azurewebsites.net/jokes/${jokeId}`, {
status: 500,
});
const consoleSpy = jest.spyOn(console, 'log');
consoleSpy.mockImplementation(() => {});
const store = mockStore({});
// @ts-ignore
await store.dispatch(deleteItem(jokeId));
expect(consoleSpy).toHaveBeenCalledWith('Erreur lors de la requête DELETE');
consoleSpy.mockRestore();
});
});

@ -1,47 +0,0 @@
// Fonction pour ajouter une blague aux favoris
import AsyncStorage from "@react-native-async-storage/async-storage";
import {SampleJoke} from "../../model/SampleJoke";
import {CustomJoke} from "../../model/CustomJoke";
const addFavorite = async (joke: SampleJoke | CustomJoke) => {
try {
let favorites: { sampleJokes: SampleJoke[], customJokes: CustomJoke[] } = await AsyncStorage.getItem('@favorites');
if (!favorites) {
favorites = { sampleJokes: [], customJokes: [] };
}
if (joke instanceof SampleJoke) {
favorites.sampleJokes.push(joke);
} else if (joke instanceof CustomJoke) {
favorites.customJokes.push(joke);
}
await AsyncStorage.setItem('@favorites', JSON.stringify(favorites));
} catch (error) {
console.error('Error adding favorite:', error);
}
};
const removeFavorite = async (jokeId: string | number) => {
try {
var favorites: { sampleJokes: SampleJoke[], customJokes: CustomJoke[] } = await AsyncStorage.getItem('@favorites');
if (!favorites) {
return;
}
favorites.sampleJokes = favorites.sampleJokes.filter(joke => joke.id !== jokeId);
favorites.customJokes = favorites.customJokes.filter(joke => joke.id !== jokeId);
await AsyncStorage.setItem('@favorites', JSON.stringify(favorites));
} catch (error) {
console.error('Error removing favorite:', error);
}
};
const getFavorites = async () => {
try {
const favoritesString = await AsyncStorage.getItem('@favorites');
if (favoritesString !== null) {
return JSON.parse(favoritesString);
}
} catch (error) {
console.error('Error getting favorites:', error);
}
return { sampleJokes: [], customJokes: [] };
};

@ -6,21 +6,19 @@ import {setCustomJokeById} from "../actions/CustomJoke";
export const getItem = <TItem>(uri:string, constructor : (json:any) => TItem, setItem: (item: TItem) => any) => { export const getItem = <TItem>(uri:string, constructor : (json:any) => TItem, setItem: (item: TItem) => any) => {
return async dispatch => { return async dispatch => {
try { try {
console.log(";;;;;;;;;;;;;;", uri, ";;;;;;;;;;;;;;;;;;;")
const promise = await fetch(uri); const promise = await fetch(uri);
const Json = await promise.json(); const Json = await promise.json();
const Item: TItem = constructor(Json); const Item: TItem = constructor(Json);
console.log("===========", Item , "===================");
dispatch(setItem(Item)); dispatch(setItem(Item));
} catch (error) { } catch (error) {
console.log('Error---------', error); console.log('Error---------', error);
} }
} }
} }
export const getJokeById = (id : number) => { export const getJokeById = (id) => {
return getItem<SampleJoke>('https://iut-weather-api.azurewebsites.net/jokes/samples/' + id.toString() , (elt) => new SampleJoke(elt["id"], elt["type"], elt["setup"], elt["image"]), (item) => setJokeById(item)) return getItem<SampleJoke>('https://iut-weather-api.azurewebsites.net/jokes/samples/' + id , (elt) => new SampleJoke(elt["id"], elt["type"], elt["setup"], elt["image"]), (item) => setJokeById(item))
} }
export const getCustomJokeById = (id: string) => { export const getCustomJokeById = (id) => {
return getItem<CustomJoke>('https://iut-weather-api.azurewebsites.net/jokes/' + id , (elt) => new CustomJoke(elt["id"], elt["type"], elt["setup"], elt["image"]), (item) => setCustomJokeById(item)) return getItem<CustomJoke>('https://iut-weather-api.azurewebsites.net/jokes/' + id , (elt) => new CustomJoke(elt["id"], elt["type"], elt["setup"], elt["image"]), (item) => setCustomJokeById(item))
} }

@ -1,81 +0,0 @@
import fetchMock from 'fetch-mock';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import { getItem, getJokeById, getCustomJokeById } from './GetByThunk'; // Assurez-vous d'importer correctement vos thunks
import { setJokeById } from '../actions/SampleAction';
import {setCustomJokeById} from "../actions/CustomJoke";
import { SampleJoke } from '../../model/SampleJoke';
import { CustomJoke } from '../../model/CustomJoke';
import {afterEach, describe, expect, it, jest} from "@jest/globals";
const middlewares = [thunk];
// @ts-ignore
const mockStore = configureMockStore(middlewares);
describe('getItem Thunk', () => {
afterEach(() => {
fetchMock.restore();
});
it('dispatches setJokeById after successful GET request for SampleJoke', async () => {
const jokeId = 1;
const expectedJoke = new SampleJoke(jokeId, 'type', 'setup', 'image');
fetchMock.getOnce(`https://iut-weather-api.azurewebsites.net/jokes/samples/${jokeId}`, {
status: 200,
body: {
id: jokeId,
type: 'type',
setup: 'setup',
image: 'image',
},
});
const store = mockStore({});
// @ts-ignore
await store.dispatch(getJokeById(parseInt(jokeId)));
expect(store.getActions()).toEqual([setJokeById(expectedJoke)]);
});
it('dispatches setCustomJokeById after successful GET request for CustomJoke', async () => {
const jokeId = '1';
const expectedJoke = new CustomJoke(jokeId, 'type', 'setup', 'image');
fetchMock.getOnce(`https://iut-weather-api.azurewebsites.net/jokes/${jokeId}`, {
status: 200,
body: {
id: jokeId,
type: 'type',
setup: 'setup',
image: 'image',
},
});
const store = mockStore({});
// @ts-ignore
await store.dispatch(getCustomJokeById(jokeId));
expect(store.getActions()).toEqual([setCustomJokeById(expectedJoke)]);
});
it('logs an error message if GET request fails', async () => {
const jokeId = '1';
fetchMock.getOnce(`https://iut-weather-api.azurewebsites.net/jokes/samples/${jokeId}`, {
status: 500,
});
const consoleSpy = jest.spyOn(console, 'log');
consoleSpy.mockImplementation(() => {});
const store = mockStore({});
// @ts-ignore
await store.dispatch(getJokeById(parseInt(jokeId)));
expect(consoleSpy).toHaveBeenCalledWith('Error---------');
});
});

@ -13,7 +13,7 @@ export const getList = <TList>(uri:string, constructor : (json:any) => TList, se
const List: TList[] = listJson.map(elt => constructor(elt)); const List: TList[] = listJson.map(elt => constructor(elt));
dispatch(setList(List)); dispatch(setList(List));
} catch (error) { } catch (error) {
console.log(`Error In getList ${uri} ---------`, error); console.log('Error---------', error);
} }
} }
} }

@ -1,84 +0,0 @@
import fetchMock from 'fetch-mock';
import configureMockStore from 'redux-mock-store';
import { getLastSampleJokesList, getCategoriesList } from './GetThunk';
import { setCategoriesList } from '../actions/CategoryAction';
import { SampleJoke } from '../../model/SampleJoke';
import { Category } from '../../model/Category';
import {afterEach, describe, expect, it, jest} from "@jest/globals";
import {setJokesList} from "../actions/SampleAction";
import thunk from "redux-thunk";
const middlewares = [thunk];
// @ts-ignore
const mockStore = configureMockStore(middlewares);
describe('getList Thunk', () => {
afterEach(() => {
fetchMock.restore();
});
it('dispatches setJokesList after successful GET request for LastSampleJokesList', async () => {
const expectedJokesList = [
new SampleJoke('1', 'type1', 'setup1', 'image1'),
new SampleJoke('2', 'type2', 'setup2', 'image2'),
];
fetchMock.getOnce('https://iut-weather-api.azurewebsites.net/jokes/lasts', {
status: 200,
body: expectedJokesList.map(joke => ({
id: joke.id,
type: joke.type,
setup: joke.setup,
image: joke.image,
})),
});
const store = mockStore({});
// @ts-ignore
await store.dispatch(getLastSampleJokesList());
expect(store.getActions()).toEqual([setJokesList(expectedJokesList)]);
});
it('dispatches setCategoriesList after successful GET request for CategoriesList', async () => {
const expectedCategoriesList = [
new Category('Category1', 1),
new Category('Category2', 2),
];
fetchMock.getOnce('https://iut-weather-api.azurewebsites.net/jokes/categories/top', {
status: 200,
body: expectedCategoriesList.map(category => ({
name: category.name,
number: category.number,
})),
});
const store = mockStore({});
// @ts-ignore
await store.dispatch(getCategoriesList());
expect(store.getActions()).toEqual([setCategoriesList(expectedCategoriesList)]);
});
// Tests similaires pour les autres fonctions thunks
it('logs an error message if GET request fails', async () => {
fetchMock.getOnce('https://iut-weather-api.azurewebsites.net/jokes/lasts', {
status: 500,
});
const consoleSpy = jest.spyOn(console, 'log');
consoleSpy.mockImplementation(() => {});
const store = mockStore({});
// @ts-ignore
await store.dispatch(getLastSampleJokesList());
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Error In getList'));
});
});

@ -1,4 +1,7 @@
import {setPostJoke} from "../actions/CustomJoke"; import {setPostJoke} from "../actions/CustomJoke";
import {SampleJoke} from "../../model/SampleJoke";
import {setJokesList} from "../actions/SampleAction";
import {getList} from "./GetThunk";
export const setItem = <TItem>( export const setItem = <TItem>(
uri: string, uri: string,

@ -1,67 +0,0 @@
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import fetchMock from 'fetch-mock';
import { setPostJoke } from '../actions/CustomJoke';
import { setItem, postCustomJoke } from './PostThunk';
import {afterEach, describe, expect, it, jest} from "@jest/globals";
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('setItem Thunk', () => {
afterEach(() => {
fetchMock.restore();
});
it('dispatches setPostJoke after successful POST request', async () => {
const mockResponse = { id: '123', type: 'pun', setup: 'Why was the math book sad?', punchline: 'Because it had too many problems.' };
fetchMock.postOnce('https://iut-weather-api.azurewebsites.net/jokes', {
body: mockResponse,
headers: { 'content-type': 'application/json' }
});
const expectedActions = [
setPostJoke(mockResponse)
];
const store = mockStore({});
await store.dispatch(setItem('https://iut-weather-api.azurewebsites.net/jokes', 'pun', 'Why was the math book sad?', 'Because it had too many problems.'));
expect(store.getActions()).toEqual(expectedActions);
});
it('logs an error message if POST request fails', async () => {
fetchMock.postOnce('https://iut-weather-api.azurewebsites.net/jokes', 404);
const consoleSpy = jest.spyOn(console, 'log');
consoleSpy.mockImplementation(() => {});
const store = mockStore({});
await store.dispatch(setItem('https://iut-weather-api.azurewebsites.net/jokes', 'pun', 'Why was the math book sad?', 'Because it had too many problems.'));
expect(consoleSpy).toHaveBeenCalledWith('Erreur lors de la requête POST');
consoleSpy.mockRestore();
});
});
describe('postCustomJoke Thunk', () => {
afterEach(() => {
fetchMock.restore();
});
it('calls setItem with correct parameters', async () => {
const uri = 'https://iut-weather-api.azurewebsites.net/jokes';
const type = 'pun';
const setup = 'Why was the math book sad?';
const punchline = 'Because it had too many problems.';
const store = mockStore({});
const setItemSpy = jest.spyOn(global, 'setItem');
setItemSpy.mockResolvedValueOnce();
await store.dispatch(postCustomJoke(type, setup, punchline));
expect(setItemSpy).toHaveBeenCalledWith(uri, type, setup, punchline);
setItemSpy.mockRestore();
});
});

@ -1,21 +0,0 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import {Theme} from "@react-navigation/native";
export const storeTheme = async (theme) => {
try {
const jsonValue = JSON.stringify(theme)
await AsyncStorage.setItem('@theme', jsonValue)
console.log("theme stored")
} catch (e) {
console.log(e);
}
}
export const getTheme = async () => {
try {
const jsonValue = await AsyncStorage.getItem('@theme')
return jsonValue != null ? JSON.parse(jsonValue) as Theme : null;
} catch(e) {
console.log(e);
}
}

@ -1,59 +0,0 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { storeTheme, getTheme } from "./ThemeThunk";
import {afterEach, describe, expect, it, jest} from "@jest/globals";
describe('storeTheme Function', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('stores theme in AsyncStorage', async () => {
const theme = { colors: { background: 'white', text: 'black' } };
const jsonValue = JSON.stringify(theme);
AsyncStorage.setItem = jest.fn().mockResolvedValueOnce();
console.log = jest.fn();
await storeTheme(theme);
expect(AsyncStorage.setItem).toHaveBeenCalledWith('@theme', jsonValue);
expect(console.log).toHaveBeenCalledWith("theme stored");
});
it('logs error if theme storage fails', async () => {
const theme = { colors: { background: 'white', text: 'black' } };
AsyncStorage.setItem = jest.fn().mockRejectedValueOnce('Storage error');
console.error = jest.fn();
await storeTheme(theme);
expect(console.error).toHaveBeenCalledWith('Storage error');
});
});
describe('getTheme Function', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('retrieves theme from AsyncStorage', async () => {
const theme = { colors: { background: 'white', text: 'black' } };
const jsonValue = JSON.stringify(theme);
AsyncStorage.getItem = jest.fn().mockResolvedValueOnce(jsonValue);
const result = await getTheme();
expect(result).toEqual(theme);
});
it('returns null if theme is not found in AsyncStorage', async () => {
AsyncStorage.getItem = jest.fn().mockResolvedValueOnce(null);
const result = await getTheme();
expect(result).toBeNull();
});
it('logs error if theme retrieval fails', async () => {
AsyncStorage.getItem = jest.fn().mockRejectedValueOnce('Retrieval error');
console.error = jest.fn();
await getTheme();
expect(console.error).toHaveBeenCalledWith('Retrieval error');
});
});

@ -5,10 +5,9 @@ import '../types/extension';
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import {Image, StyleSheet, Text, TextInput, TouchableOpacity, View} from "react-native"; import {Image, StyleSheet, Text, TextInput, TouchableOpacity, View} from "react-native";
import {darksalmonColor, greyColor, indigoColor, purpleColor, whiteColor} from "../assets/Theme"; import {darksalmonColor, greyColor, indigoColor, purpleColor, whiteColor} from "../assets/Theme";
import {getCustomJokesList, getSampleJokesList} from "../redux/thunk/GetThunk"; import {getSampleJokesList} from "../redux/thunk/GetThunk";
import React, {useEffect} from "react"; import React, {useEffect} from "react";
import {postCustomJoke} from "../redux/thunk/PostThunk"; import {postCustomJoke} from "../redux/thunk/PostThunk";
import {useTheme} from "@react-navigation/native";
export default function AddScreen() { export default function AddScreen() {
// @ts-ignore // @ts-ignore
@ -26,14 +25,9 @@ export default function AddScreen() {
const handleCreate = () => { const handleCreate = () => {
// @ts-ignore // @ts-ignore
dispatch(postCustomJoke(joke, downgrade, category)); dispatch(postCustomJoke(joke, downgrade, category));
// @ts-ignore
dispatch(getCustomJokesList());
clearFields(); clearFields();
}; };
const styles = themeSettings()
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.text}>Blague</Text> <Text style={styles.text}>Blague</Text>
@ -88,19 +82,16 @@ export default function AddScreen() {
) )
}; };
export function themeSettings() {
const theme = useTheme();
const colors = theme.colors;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: colors.background,
backgroundColor: purpleColor,
width: "100%", width: "100%",
height: "100%", height: "100%",
}, },
textInput: { textInput: {
backgroundColor: colors.card, backgroundColor: indigoColor,
color: colors.text, color: whiteColor,
width: "90%", width: "90%",
alignSelf:"center", alignSelf:"center",
minHeight: 100 minHeight: 100
@ -108,14 +99,14 @@ export function themeSettings() {
eraseButton:{ eraseButton:{
borderRadius : 5, borderRadius : 5,
alignItems: "center", alignItems: "center",
backgroundColor: colors.primary, backgroundColor : whiteColor,
height:50, height:50,
margin: 10 margin: 10
}, },
createButton:{ createButton:{
borderRadius : 5, borderRadius : 5,
alignItems: "center", alignItems: "center",
backgroundColor: colors.primary, backgroundColor : darksalmonColor,
height:50, height:50,
margin: 10, margin: 10,
marginTop: 30 marginTop: 30
@ -124,16 +115,16 @@ export function themeSettings() {
margin: 10, margin: 10,
textAlign : "center", textAlign : "center",
fontWeight: "700", fontWeight: "700",
color: colors.text, color : whiteColor,
}, },
eraseTextButton : { eraseTextButton : {
margin: 10, margin: 10,
textAlign : "center", textAlign : "center",
fontWeight: "700", fontWeight: "700",
color: colors.text, color : darksalmonColor,
}, },
text: { text: {
color: colors.text, color:whiteColor,
paddingBottom: 10, paddingBottom: 10,
paddingTop: 25, paddingTop: 25,
marginLeft: 19, marginLeft: 19,
@ -143,7 +134,7 @@ export function themeSettings() {
paddingTop: 15, paddingTop: 15,
marginRight: 19, marginRight: 19,
fontSize: 12, fontSize: 12,
color: colors.text, color: whiteColor,
}, },
viewCounter: { viewCounter: {
alignItems: 'flex-end', alignItems: 'flex-end',
@ -151,5 +142,3 @@ export function themeSettings() {
right: 10, right: 10,
} }
}) })
return styles
}

@ -5,42 +5,34 @@ import {darksalmonColor, purpleColor, whiteColor} from "../assets/Theme";
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {getCustomJokesList, getSampleJokesList} from "../redux/thunk/GetThunk"; import {getCustomJokesList, getSampleJokesList} from "../redux/thunk/GetThunk";
import {useTheme} from "@react-navigation/native";
const eye = require("../assets/eye_icon.png")
const hideEye = require("../assets/eye_off_icon.png")
export default function Catalogue() { export default function Catalogue() {
// @ts-ignore // @ts-ignore
const sampleJokes = useSelector(state => state.sampleReducer.jokes); const sampleJokes = useSelector(state => state.sampleReducer.jokes);
// @ts-ignore // @ts-ignore
const customJokes = useSelector(state => state.customReducer.customJokes); const customJokes = useSelector(state => state.customReducer.customJokes);
const [joke, setJoke] = useState([])
const dispatch = useDispatch(); const dispatch = useDispatch();
const eye = require("../assets/eye_icon.png")
const [showCustomJoke, setCustomJoke] = useState(false); const hideEye = require("../assets/eye_off_icon.png")
const toggleDescription = () => {
setCustomJoke(!showCustomJoke);
};
useEffect(() => { useEffect(() => {
if(!showCustomJoke) {
const loadSamplesJokes = async () => { const loadSamplesJokes = async () => {
// @ts-ignore // @ts-ignore
await dispatch(getSampleJokesList()); await dispatch(getSampleJokesList());
}; };
loadSamplesJokes(); loadSamplesJokes();
setJoke(sampleJokes) }, [dispatch]);
} else {
const loadCustomJokes = async () => { const loadCustomJokes = async () => {
// @ts-ignore // @ts-ignore
await dispatch(getCustomJokesList()); await dispatch(getCustomJokesList());
}; };
loadCustomJokes();
setJoke(customJokes)
}
}, [dispatch, customJokes, sampleJokes]);
const styles = themeSettings() const [showCustomJoke, setCustomJoke] = useState(false); // état local pour contrôler la visibilité de la description
const toggleDescription = () => {
setCustomJoke(!showCustomJoke);
loadCustomJokes();
};
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
@ -56,23 +48,20 @@ export default function Catalogue() {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={styles.container}> <View style={styles.container}>
<JokeItems jokes={joke}/> <JokeItems jokes={showCustomJoke ? customJokes : sampleJokes}/>
</View> </View>
</SafeAreaView> </SafeAreaView>
) )
}; };
export function themeSettings() {
const theme = useTheme();
const colors = theme.colors;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: colors.background, backgroundColor: purpleColor,
flex:1, flex:1,
}, },
Button:{ Button:{
borderRadius : 5, borderRadius : 5,
backgroundColor: colors.primary, backgroundColor : darksalmonColor,
height:40, height:40,
width : 60, width : 60,
flexDirection : "row" flexDirection : "row"
@ -90,13 +79,13 @@ export function themeSettings() {
top : 5, top : 5,
alignSelf : "center", alignSelf : "center",
backgroundColor: "none", backgroundColor: "none",
tintColor: colors.notification, tintColor : whiteColor,
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}, },
TextButton : { TextButton : {
fontSize: 18, fontSize: 18,
color: colors.text, color: whiteColor,
textAlign: 'center', textAlign: 'center',
fontWeight: 'bold', fontWeight: 'bold',
margin: 10, margin: 10,
@ -110,5 +99,3 @@ export function themeSettings() {
alignItems: "center", alignItems: "center",
} }
}); });
return styles
}

@ -1,115 +1,21 @@
import { customJokeStub } from '../data/stub/CustomJokeStub';
import { sampleJokeStub } from '../data/stub/SampleJokeStub';
import JokeItems from "../components/JokeItems"; import JokeItems from "../components/JokeItems";
import '../types/extension'; import '../types/extension';
import {Image, SafeAreaView, StyleSheet, Text, TouchableOpacity, View} from "react-native"; import {StyleSheet, View} from "react-native";
import {darksalmonColor, purpleColor, whiteColor} from "../assets/Theme"; import {purpleColor} from "../assets/Theme";
import {useDispatch, useSelector} from "react-redux";
import React, {useEffect, useState} from "react";
import {getCustomJokesList, getSampleJokesList} from "../redux/thunk/GetThunk";
import {useTheme} from "@react-navigation/native";
export default function Favorites() {
// @ts-ignore
const sampleJokes = useSelector(state => state.sampleReducer.jokes);
// @ts-ignore
const customJokes = useSelector(state => state.customReducer.customJokes);
const [joke, setJoke] = useState([])
const dispatch = useDispatch();
const eye = require("../assets/eye_icon.png")
const hideEye = require("../assets/eye_off_icon.png")
const [showCustomJoke, setCustomJoke] = useState(false); // état local pour contrôler la visibilité de la description
const toggleDescription = () => {
setCustomJoke(!showCustomJoke);
};
useEffect(() => {
if(!showCustomJoke) {
const loadSamplesJokes = async () => {
// @ts-ignore
await dispatch(getFavorites());
};
loadSamplesJokes();
setJoke(sampleJokes)
} else {
const loadCustomJokes = async () => {
// @ts-ignore
await dispatch(getCustomJokesList());
};
loadCustomJokes();
setJoke(customJokes)
}
}, [dispatch, customJokes, sampleJokes]);
const styles = themeSettings()
export default function Catalogue() {
const allJokes = [...customJokeStub, ...sampleJokeStub];
return ( return (
<SafeAreaView style={styles.container}>
<View style={styles.columnContainer}>
<Text style={styles.TextButton}>Afficher les exemples</Text>
<TouchableOpacity style={styles.Button} onPress={toggleDescription}>
<View style={styles.jokeTypeContainer}>
<Image
source={showCustomJoke ? hideEye : eye}
style={styles.imageButton}
/>
</View>
</TouchableOpacity>
</View>
<View style={styles.container}> <View style={styles.container}>
<JokeItems jokes={joke}/> <JokeItems jokes={allJokes}/>
</View> </View>
</SafeAreaView>
) )
}; };
export function themeSettings() {
const theme = useTheme();
const colors = theme.colors;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: colors.background, backgroundColor: purpleColor
flex: 1,
},
Button: {
borderRadius: 5,
backgroundColor: colors.background,
height: 40,
width: 60,
flexDirection: "row"
},
jokeTypeContainer: {
display: "flex",
flex: 1,
flexDirection: "row",
alignItems: "center"
},
imageButton: {
margin: 10,
width: 40,
height: 30,
top: 5,
alignSelf: "center",
backgroundColor: "none",
tintColor: colors.notification,
justifyContent: "center",
alignItems: "center",
},
TextButton: {
fontSize: 18,
color: colors.notification,
textAlign: 'center',
fontWeight: 'bold',
margin: 10,
},
columnContainer: {
marginLeft: 20,
marginRight: 20,
width: '90%',
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
} }
}); });
return styles
}

@ -6,8 +6,8 @@ import Categs from "../components/Categs";
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import {useEffect} from "react"; import {useEffect} from "react";
import {getCategoriesList, getLastSampleJokesList} from "../redux/thunk/GetThunk"; import {getCategoriesList, getLastSampleJokesList} from "../redux/thunk/GetThunk";
import {useTheme} from "@react-navigation/native"; import SampleReducer from "../redux/reducers/SampleReducer";
import styleToBarStyle from "expo-status-bar/build/styleToBarStyle"; import CategoryReducer from "../redux/reducers/CategoryReducer";
export default function Catalogue() { export default function Catalogue() {
// @ts-ignore // @ts-ignore
@ -25,9 +25,6 @@ export default function Catalogue() {
}; };
loadJokes(); loadJokes();
}, [dispatch]); }, [dispatch]);
const styles = themeSettings()
return ( return (
<> <>
<View style={styles.container}> <View style={styles.container}>
@ -53,12 +50,9 @@ export default function Catalogue() {
) )
}; };
export function themeSettings() {
const theme = useTheme();
const colors = theme.colors;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: colors.background, backgroundColor: purpleColor,
flex:1, flex:1,
}, },
top: { top: {
@ -70,7 +64,7 @@ export function themeSettings() {
}, },
textAccueil: { textAccueil: {
fontSize: 25, fontSize: 25,
color: colors.text, color: darksalmonColor,
fontWeight: "bold", fontWeight: "bold",
}, },
Jokes: { Jokes: {
@ -83,7 +77,7 @@ export function themeSettings() {
alignItems: "center" alignItems: "center"
}, },
textLastJokes: { textLastJokes: {
color: colors.border, color: whiteColor,
fontSize: 20, fontSize: 20,
fontWeight: "bold", fontWeight: "bold",
}, },
@ -95,7 +89,7 @@ export function themeSettings() {
flexDirection: "row", flexDirection: "row",
}, },
textBestCateg: { textBestCateg: {
color: colors.text, color: whiteColor,
fontSize: 20, fontSize: 20,
fontWeight: "bold", fontWeight: "bold",
}, },
@ -104,5 +98,3 @@ export function themeSettings() {
marginTop: 30, marginTop: 30,
} }
}); });
return styles
}

@ -5,54 +5,43 @@ import {useDispatch, useSelector} from "react-redux";
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {getCustomJokeById, getJokeById} from "../redux/thunk/GetByThunk"; import {getCustomJokeById, getJokeById} from "../redux/thunk/GetByThunk";
import JokeDetail from "../components/JokeDetail"; import JokeDetail from "../components/JokeDetail";
import {AppDispatch, AppState} from "../redux/store";
import {CustomJoke} from "../model/CustomJoke";
import {SampleJoke} from "../model/SampleJoke";
import {useTheme} from "@react-navigation/native";
export default function JokeDetailsScreen({route}) { export default function JokeDetailsScreen({route}) {
const idJokeDetail = route.params.idJoke; const idJokeDetail = route.params.idJoke;
const joke = useSelector<AppState>(state => state.sampleReducer.joke); // @ts-ignore
const customJoke = useSelector<AppState>(state => state.customReducer.customJoke); const joke = useSelector(state => state.sampleReducer.jokes);
const [isCustomJoke, setCustomJoke] = useState(false); // @ts-ignore
const dispatch = useDispatch<AppDispatch>(); const customJoke = useSelector(state => state.customReducer.customJoke);
const [isCustomJoke, setCustomJoke] = useState(false); // état local pour contrôler la visibilité de la description
const styles = themeSettings(); // @ts-ignore
const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
if(typeof idJokeDetail == 'number') { if(idJokeDetail instanceof Number) {
const loadJoke = async () => { const loadJoke = async () => {
// @ts-ignore
await dispatch(getJokeById(idJokeDetail)); await dispatch(getJokeById(idJokeDetail));
}; };
loadJoke(); loadJoke();
setCustomJoke(!isCustomJoke) setCustomJoke(!isCustomJoke)
} else { } else {
const loadCustomJoke = async () => { const loadCustomJoke = async () => {
// @ts-ignore
await dispatch(getCustomJokeById(idJokeDetail)); await dispatch(getCustomJokeById(idJokeDetail));
}; };
loadCustomJoke(); loadCustomJoke();
} }
}, [dispatch]); }, [dispatch]);
if(!joke && !customJoke) return null;
return ( return (
<View style={styles.container}> <View style={styles.container}>
{/*@ts-ignore}*/}
<JokeDetail joke={isCustomJoke ? joke : customJoke}/> <JokeDetail joke={isCustomJoke ? joke : customJoke}/>
</View> </View>
) )
}; };
export function themeSettings() {
const theme = useTheme();
const colors = theme.colors;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: colors.background, backgroundColor: purpleColor,
flex:1, flex:1,
} }
}); });
return styles
}

@ -1,102 +1,21 @@
import { customJokeStub } from '../data/stub/CustomJokeStub';
import { sampleJokeStub } from '../data/stub/SampleJokeStub';
import JokeItems from "../components/JokeItems";
import '../types/extension'; import '../types/extension';
import {Image, StyleSheet, Switch, Text, View} from "react-native"; import {StyleSheet, View} from "react-native";
import { import {purpleColor} from "../assets/Theme";
darksalmonColor, DarkTheme,
whiteColor
} from "../assets/Theme";
import React from "react";
import {DefaultTheme, useTheme} from "@react-navigation/native";
import {storeTheme} from "../redux/thunk/ThemeThunk";
export default function Catalogue() { export default function Catalogue() {
const light_mode = require("../assets/light_mode.png") const allJokes = [...customJokeStub, ...sampleJokeStub];
const dark_mode = require("../assets/dark_mode.png")
const [isDark, setDark] = React.useState(false)
const toggleTheme = () => {
setDark(previousState => {
const theme = !previousState;
const newTheme = theme ? DarkTheme : DefaultTheme;
storeTheme(newTheme);
return theme;
});
};
const styles = themeSettings();
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={styles.topText}> <JokeItems jokes={allJokes}/>
<Text style={styles.title}>Réglages</Text>
<View style={styles.switchMode}>
<View style={styles.textContainer}>
<Image
source={isDark? dark_mode: light_mode}
style={styles.imageButton} />
<Text style={styles.darkModeText}>Dark Mode</Text>
</View>
<Switch
value={isDark}
onValueChange={toggleTheme}
style={styles.switchMode}
trackColor={{false: darksalmonColor, true: darksalmonColor}}
thumbColor={whiteColor} />
</View> </View>
</View> )
</View>
);
}; };
export function themeSettings() {
const theme = useTheme();
const colors = theme.colors;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
paddingTop: 10, backgroundColor: purpleColor
paddingBottom: 10,
paddingLeft: 10,
paddingRight: 10,
flex: 1,
justifyContent: 'center',
backgroundColor: colors.background,
flexDirection: 'column',
},
topText: {
flex: 1,
},
title: {
padding: 10,
fontSize: 20,
color: colors.text,
fontWeight: 'bold'
},
imageButton : {
width: 30,
height:30,
alignSelf : "center",
tintColor: colors.primary,
},
switchMode: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: colors.card,
padding: 20,
margin: 10,
},
darkModeText: {
color: colors.text,
fontSize: 20,
marginLeft: 10,
paddingTop: 5,
},
textContainer: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
} }
}); });
return styles;
}

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