Compare commits

...

35 Commits

Author SHA1 Message Date
Antoine PEREDERII f11ca02ec4 Merge branch 'master' of codefirst.iut.uca.fr:antoine.perederii/TpReactNative
continuous-integration/drone/push Build is passing Details
1 year ago
Antoine PEREDERII a043f05832 🧪 Add some tests
1 year ago
Antoine PEREDERII 0eee3563de Update '.drone.yml'
continuous-integration/drone/push Build is passing Details
1 year ago
Antoine PEREDERII d5786ea613 Update '.drone.yml'
continuous-integration/drone/push Build is passing Details
1 year ago
Antoine PEREDERII cde37fcdd8 👷 Sonar deployement tests
1 year ago
Antoine PEREDERII 22d856df67 Merge pull request '🔥 Tests not concluent of Favorites local storage' (#11) from part8 into master
1 year ago
Antoine PEREDERII f6210d7825 🔥 Tests not concluent of Favorites local storage
1 year ago
Antoine PEREDERII b49beeb1e0 Update 'README.md'
1 year ago
Antoine PEREDERII 1c2518a02a Merge pull request 'part7' (#10) from part7 into master
1 year ago
Antoine PEREDERII d2cfa7b992 🐛 Update some nope imports
1 year ago
Antoine PEREDERII f6a01ff7db 🚀 Finish themes
1 year ago
Antoine PEREDERII 6bd58278a0 Merge pull request ' Add first part of theme' (#9) from part7 into master
1 year ago
Antoine PEREDERII 57a846972c Add first part of theme
1 year ago
Antoine PEREDERII 5bd99267bd Merge pull request '🔥 Remove code' (#8) from part6 into master
1 year ago
Antoine PEREDERII a4a79361f8 🔥 Remove code
1 year ago
Antoine PEREDERII e69ed4beeb Merge pull request ' Finish tp6' (#7) from part6 into master
1 year ago
Antoine PEREDERII 476ecb9162 Finish tp6
1 year ago
Antoine PEREDERII 3cc455b3a5 Merge pull request 'part6' (#6) from part6 into master
1 year ago
Antoine PEREDERII ba870e4320 Introduce new TP6
1 year ago
Antoine PEREDERII f8e11adc9f Merge branch 'part5' of codefirst.iut.uca.fr:antoine.perederii/TpReactNative into part5
1 year ago
Antoine PEREDERII bf20bc3306 🔥 Update code
1 year ago
Antoine PEREDERII 723c07cc2c Update 'src/components/JokeDetail.tsx'
1 year ago
Antoine PEREDERII 7820822622 Merge pull request ' Add joke details page' (#5) from part5 into master
1 year ago
Antoine PEREDERII 0a3c15d468 🔀 Merge part5 on master
1 year ago
Antoine PEREDERII ac58bf5302 Add joke details page
1 year ago
Antoine PEREDERII d2d7449d2f Merge pull request 'part4' (#4) from part4 into master
1 year ago
Antoine PEREDERII 79e58e2c80 Update 'src/redux/thunk/RecentsJokesThunk.ts'
1 year ago
Antoine PEREDERII 56558a43bc 🏗️ Add API link
1 year ago
Antoine PEREDERII 1910234f06 Merge pull request 'part4' (#3) from part4 into master
1 year ago
Antoine PEREDERII b2a19dcd44 Add API requests
1 year ago
Antoine PEREDERII 086ea9fd3d ♻️ Update categ filter id to type
1 year ago
Antoine PEREDERII 37f093fc75 💄 Update UI code, remove somes un-used import
1 year ago
Antoine PEREDERII 6195b1a95e 🎨 Update folders project and add a first part of tp1
1 year ago
Antoine PEREDERII dab1695919 Update UI of the part2
1 year ago
Antoine PEREDERII b23e3b703a Merge pull request 'part2 merge on master' (#1) from part2 into master
1 year ago

@ -0,0 +1,17 @@
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 5 - FlatList
* [X] Partie 6 - Safe Area
* [ ] Partie 7 - Navigation
* [ ] Partie 8 - Hooks
* [ ] Partie 9 - Redux Store
* [ ] Partie 10 - Async Storage
* [ ] Partie 11 - Theming
* [X] Partie 7 - Navigation
* [X] Partie 8 - Hooks
* [X] Partie 9 - Redux Store
* [X] Partie 10 - Async Storage
* [X] Partie 11 - Theming
* [ ] Partie 12 - Unit testing
* [ ] Partie 13 - Resources

@ -0,0 +1,8 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.
> What do the files contain?
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
- "settings.json": contains the server configuration that is used to serve the application manifest.
> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.

@ -0,0 +1,3 @@
{
"devices": []
}

@ -0,0 +1 @@
module.exports = $$require_external('node:assert');

@ -0,0 +1 @@
module.exports = $$require_external('node:async_hooks');

@ -0,0 +1 @@
module.exports = $$require_external('node:buffer');

@ -0,0 +1 @@
module.exports = $$require_external('node:child_process');

@ -0,0 +1 @@
module.exports = $$require_external('node:cluster');

@ -0,0 +1 @@
module.exports = $$require_external('node:console');

@ -0,0 +1 @@
module.exports = $$require_external('node:constants');

@ -0,0 +1 @@
module.exports = $$require_external('node:crypto');

@ -0,0 +1 @@
module.exports = $$require_external('node:dgram');

@ -0,0 +1 @@
module.exports = $$require_external('node:diagnostics_channel');

@ -0,0 +1 @@
module.exports = $$require_external('node:dns');

@ -0,0 +1 @@
module.exports = $$require_external('node:domain');

@ -0,0 +1 @@
module.exports = $$require_external('node:events');

@ -0,0 +1 @@
module.exports = $$require_external('node:fs');

@ -0,0 +1 @@
module.exports = $$require_external('node:fs/promises');

@ -0,0 +1 @@
module.exports = $$require_external('node:http');

@ -0,0 +1 @@
module.exports = $$require_external('node:http2');

@ -0,0 +1 @@
module.exports = $$require_external('node:https');

@ -0,0 +1 @@
module.exports = $$require_external('node:inspector');

@ -0,0 +1 @@
module.exports = $$require_external('node:module');

@ -0,0 +1 @@
module.exports = $$require_external('node:net');

@ -0,0 +1 @@
module.exports = $$require_external('node:os');

@ -0,0 +1 @@
module.exports = $$require_external('node:path');

@ -0,0 +1 @@
module.exports = $$require_external('node:perf_hooks');

@ -0,0 +1 @@
module.exports = $$require_external('node:process');

@ -0,0 +1 @@
module.exports = $$require_external('node:punycode');

@ -0,0 +1 @@
module.exports = $$require_external('node:querystring');

@ -0,0 +1 @@
module.exports = $$require_external('node:readline');

@ -0,0 +1 @@
module.exports = $$require_external('node:repl');

@ -0,0 +1 @@
module.exports = $$require_external('node:stream');

@ -0,0 +1 @@
module.exports = $$require_external('node:string_decoder');

@ -0,0 +1 @@
module.exports = $$require_external('node:timers');

@ -0,0 +1 @@
module.exports = $$require_external('node:tls');

@ -0,0 +1 @@
module.exports = $$require_external('node:trace_events');

@ -0,0 +1 @@
module.exports = $$require_external('node:tty');

@ -0,0 +1 @@
module.exports = $$require_external('node:url');

@ -0,0 +1 @@
module.exports = $$require_external('node:util');

@ -0,0 +1 @@
module.exports = $$require_external('node:v8');

@ -0,0 +1 @@
module.exports = $$require_external('node:vm');

@ -0,0 +1 @@
module.exports = $$require_external('node:wasi');

@ -0,0 +1 @@
module.exports = $$require_external('node:worker_threads');

@ -0,0 +1 @@
module.exports = $$require_external('node:zlib');

@ -0,0 +1 @@
global.$$require_external = typeof window === "undefined" ? require : () => null;

@ -0,0 +1 @@
global.$$require_external = (moduleId) => {throw new Error(`Node.js standard library module ${moduleId} is not available in this JavaScript environment`);}

@ -0,0 +1,25 @@
import { SafeAreaView } from 'react-native'
import {StyleSheet} from 'react-native';
import NavigationBar from "./navigation/NavigationBar";
import {indigoColor} from "./assets/Theme";
import store from "./redux/store";
import React from "react";
import {Provider} from "react-redux";
export default function App() {
return (
<Provider store={store}>
<SafeAreaView style={{flex: 0, backgroundColor: 'darksalmon'}}/>
<SafeAreaView style={styles.container}>
<NavigationBar />
</SafeAreaView>
</Provider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: indigoColor
},
});

@ -0,0 +1,32 @@
import {Theme} from "@react-navigation/native";
export const indigoColor = "rgba(14, 14, 44, 1)";
export const purpleColor = "rgba(74, 74, 104, 1)";
export const darksalmonColor = "rgba(233, 150, 122, 1)";
export const greyColor = "rgba(140, 140, 161, 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)',
},
};

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

@ -0,0 +1,27 @@
import {StyleSheet, Text, View} from 'react-native';
import {greyColor} from "../assets/Theme";
import {Category} from "../model/Category";
type CategItemProps = {
category: Category;
};
export default function Categ(prop: CategItemProps) {
return (
<View style={styles.bottomContainer}>
<Text style={{color:'white'}}>{prop.category.name}</Text>
</View>
);
}
const styles = StyleSheet.create({
bottomContainer: {
backgroundColor: greyColor,
paddingVertical: 5,
paddingHorizontal: 10,
margin: 10,
borderRadius: 20,
width : 120,
alignItems : 'center'
}
});

@ -0,0 +1,26 @@
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));
});
});

@ -0,0 +1,21 @@
import {FlatList} from 'react-native';
import Categ from "./Categ";
import {Category} from "../model/Category";
type CategListItemProps = {
categories: Category[];
};
export default function Categs(props: CategListItemProps) {
return (
<FlatList showsHorizontalScrollIndicator={false} horizontal={true}
data={props.categories.sort((a, b) => b.number - a.number)}
keyExtractor={(item) => item.name}
renderItem={
({ item }: { item: Category }) => (
<Categ category={item}/>
)
}
/>
);
}

@ -0,0 +1,36 @@
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);
});
});

@ -0,0 +1,178 @@
import {StyleSheet, Text, View, Image, Button, TouchableOpacity} from 'react-native';
import {SampleJoke} from "../model/SampleJoke";
import {darksalmonColor, whiteColor, greyColor, indigoColor, purpleColor} from "../assets/Theme";
import React, {useEffect, useState} from "react";
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 = {
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) {
const [showDescription, setShowDescription] = useState(false);
const [showFavorite, setShowFavorite] = useState(false);
const isCustom = props.joke instanceof CustomJoke;
const dispatch = useDispatch<AppDispatch>();
const navigation = useNavigation();
const toggleDescription = () => {
setShowDescription(!showDescription);
};
const toggleFavorite = () => {
setShowFavorite(!showFavorite);
};
const deleteCustomJokes = async () => {
await dispatch(deleteCustomJoke(props.joke.id));
await dispatch(getCustomJokesList());
navigation.goBack();
};
return(
<View style={styles.container}>
<Image source={{ uri: props.joke.image }} style={styles.image} />
<View style={styles.bottomContainer}>
<Text style={{color: indigoColor}}>{props.joke.type}</Text>
</View>
<Text style={styles.text}>Résumé de la blague</Text>
<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}>
<View>
<Image
source={showFavorite ? heartPlain : heart}
style={styles.imageButton}
/>
</View>
</TouchableOpacity>
<TouchableOpacity style={styles.Button} onPress={toggleDescription}>
<View style={styles.chuteContainer}>
<Image
source={showDescription ? hideEye : eye}
style={styles.imageButton}
/>
<Text style={styles.TextButton} >LA CHUTE</Text>
</View>
</TouchableOpacity>
</View>
{showDescription && <Text style={styles.text}>{props.joke.description()}</Text>}
</View>
);
}
const styles = StyleSheet.create({
image : {
margin : 5,
width: '90%',
height:200,
top : 5,
alignSelf : "center",
backgroundColor: "white",
borderRadius: 5,
},
Button:{
borderRadius : 5,
backgroundColor : darksalmonColor,
height:50,
width : 160,
flexDirection : "row"
},
imageButton : {
margin : 10,
width: 50,
height:50,
top : 5,
alignSelf : "center",
backgroundColor: "none",
tintColor : whiteColor
},
favContainer : {
margin : 20,
borderWidth : 3,
borderRadius : 15,
borderColor : whiteColor,
borderStyle : "solid"
},
TextButton : {
margin: 10,
textAlign : "center",
fontWeight: "700",
color : whiteColor,
},
chuteContainer :{
display : "flex",
flex : 1,
flexDirection: "row",
alignItems : "center"
},
container: {
marginHorizontal: "5%",
display: "flex",
marginBottom:7,
marginTop:7,
paddingVertical: 10,
backgroundColor: indigoColor,
justifyContent: 'space-between',
borderRadius: 20,
height: "auto",
borderColor: whiteColor,
borderStyle: "solid",
borderWidth: 1
},
row: {
display: "flex",
flexDirection:"row",
alignSelf: "flex-end",
},
color: {
flex: 0,
backgroundColor: darksalmonColor,
height: 150,
width:15,
},
columnContainer: {
flexDirection: "column",
marginLeft: 20,
marginRight: 20,
width: '60%',
flex: 2,
justifyContent: 'space-between',
},
text: {
color:greyColor,
paddingBottom: 7,
paddingTop: 7,
marginLeft: 19,
fontSize: 16,
},
bottomContainer: {
backgroundColor: whiteColor,
paddingVertical: 5,
paddingHorizontal: 10,
margin: 10,
marginLeft: 19,
marginTop: 20,
borderRadius: 20,
width : 150,
alignItems : 'center'
}
})

@ -0,0 +1,54 @@
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
});
});

@ -0,0 +1,67 @@
import {StyleSheet, Text, View, Image} from 'react-native';
import {SampleJoke} from "../model/SampleJoke";
import {CustomJoke} from "../model/CustomJoke";
import {darksalmonColor, whiteColor, indigoColor} from "../assets/Theme";
type JokeListItemProps = {
joke: (CustomJoke | SampleJoke);
};
export default function JokeHomeSquare(prop: JokeListItemProps) {
return (
<View style={styles.container}>
<View style={styles.topBackgroundColor}>
<View style={{width: 200, height: 40, backgroundColor: darksalmonColor}}/>
</View>
<View style={styles.bottomBackgroundColor}>
<View style={{width: 200, height: 120, backgroundColor: indigoColor}}/>
</View>
<Image source={{ uri: prop.joke.image }} style={styles.image} />
<Text style={[styles.text, styles.textTitle]}>Résumé de la blague</Text>
<Text style={[styles.text, styles.textSimple]}>{prop.joke.description()}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginTop: 10,
marginLeft: 20,
position: "relative",
},
topBackgroundColor: {
borderTopLeftRadius: 5,
borderTopRightRadius: 5,
overflow: 'hidden',
},
bottomBackgroundColor: {
borderBottomLeftRadius: 5,
borderBottomRightRadius: 5,
overflow: 'hidden',
},
image: {
width: 120,
height: 75,
position: "absolute",
top: 5,
left: "50%",
marginLeft: -60
},
text: {
position: 'absolute',
textAlign: "center",
color:whiteColor,
paddingBottom: 7,
paddingTop: 7,
width: "100%"
},
textTitle: {
fontSize: 17,
fontWeight: "bold",
top: 90
},
textSimple: {
fontSize: 15,
top: 115
}
});

@ -1,35 +1,27 @@
import {StyleSheet, Text, View, FlatList, Image} from 'react-native';
import {StyleSheet, Text, View, Image} from 'react-native';
import {SampleJoke} from "../model/SampleJoke";
import {CustomJoke} from "../model/CustomJoke";
import {darksalmonColor, whiteColor, greyColor, indigoColor} from "../assets/Theme";
import {Colors} from "react-native/Libraries/NewAppScreen";
import Categ from "./Categ";
type JokeListItemProps = {
jokes: CustomJoke[] | SampleJoke[];
joke: (CustomJoke | SampleJoke);
};
export default function JokeListItem(props: JokeListItemProps) {
const renderItem = ({ item }: { item: CustomJoke | SampleJoke }) => (
export default function JokeItem(prop: JokeListItemProps) {
return (
<View style={styles.rowContainer}>
<View style={styles.color}/>
<Image source={{ uri: item.image }} style={styles.image} />
<Image source={{ uri: prop.joke.image }} style={styles.image} />
<View style={styles.columnContainer}>
<Text style={styles.text}>Résumé de la blague</Text>
<Text style={styles.text}>{item.description()}</Text>
<Text style={styles.text}>{prop.joke.description()}</Text>
<View style={styles.bottomContainer}>
<Text style={{color:'white'}}>{item.type}</Text>
<Text style={{color:'white'}}>{prop.joke.type}</Text>
</View>
</View>
</View>
);
return (
<FlatList
data={props.jokes}
keyExtractor={(item) => item.id.toString()}
renderItem={renderItem}
/>
);
}
const styles = StyleSheet.create({
@ -74,8 +66,9 @@ const styles = StyleSheet.create({
backgroundColor: greyColor,
paddingVertical: 5,
paddingHorizontal: 10,
margin: 10,
borderRadius: 20,
width : 150,
width : 120,
alignItems : 'center'
}
});

@ -0,0 +1,35 @@
import {FlatList, Image, StyleSheet, Text, TouchableHighlight, TouchableOpacity, View} from 'react-native';
import {SampleJoke} from "../model/SampleJoke";
import {CustomJoke} from "../model/CustomJoke";
import JokeItem from "./JokeItem";
import React, {useState} from "react";
import {useNavigation} from "@react-navigation/native";
import {darksalmonColor, greyColor, indigoColor, whiteColor} from "../assets/Theme";
type JokeListItemProps = {
jokes: (CustomJoke | SampleJoke)[];
};
export default function JokeItems(props: JokeListItemProps) {
const navigation = useNavigation()
return (
<FlatList
data={props.jokes}
keyExtractor={(item) => item.id.toString()}
renderItem={
({ item }: { item: CustomJoke | SampleJoke }) => (
// @ts-ignore
<TouchableHighlight onPress={() => navigation.navigate("JokeDetails", {"idJoke": item.id})}>
<JokeItem joke={item}/>
</TouchableHighlight>
)
}
/>
);
}
const styles = StyleSheet.create({
})

@ -0,0 +1,22 @@
import {FlatList} from 'react-native';
import {SampleJoke} from "../model/SampleJoke";
import {CustomJoke} from "../model/CustomJoke";
import JokeHomeSquare from "./JokeHomeSquare";
type JokeListItemProps = {
jokes: (CustomJoke | SampleJoke)[];
};
export default function JokesHomeSquare(props: JokeListItemProps) {
return (
<FlatList showsHorizontalScrollIndicator={false} horizontal={true}
data={props.jokes}
renderItem={
({ item }: { item: CustomJoke | SampleJoke }) => (
<JokeHomeSquare joke={item}/>
)
}
keyExtractor={(item) => item.id.toString()}
/>
);
}

@ -0,0 +1,13 @@
/**
* @file CustomJokeStub.ts
* @brief Exemple d'utilisation de la classe JokeFactory pour créer des blagues personnalisées.
*/
import { JokeFactory } from '../../model/JokeFactory';
/**
* @brief Stub de blagues personnalisées créées à l'aide de la classe JokeFactory.
* @constant
* @type {CustomJoke[]}
*/
export const customJokeStub = JokeFactory.createCustomJokes('[{"id":"premier", "type":"custom", "setup":"one", "punchline":"y\'en a pas", "image":"https://placekitten.com/200/300"},{"id":"deuxieme", "type":"custom", "setup":"two","punchline":"y\'en a pas", "image":"https://placekitten.com/200/300"},{"id":"troisieme", "type":"Default", "setup":"three","punchline":"y\'en toujours a pas ;)", "image":"https://placekitten.com/200/300"},{"id":"quatrieme", "type":"custom bro", "setup":"four","punchline":"y\'en toujours toujours ap", "image":"https://placekitten.com/200/300"}]');

@ -0,0 +1,55 @@
/**
* @file Category.ts
* @brief Définition de la classe Catégory.
*/
/**
* @class
* @brief Représente une catégorie nom.
*/
export class Category {
private _name: string;
private _number: number;
/**
* @brief Constructeur de la classe Category.
* @param {string} name - Le nom de la catégorie.
* @param {number} number - Le nombre de la catégorie.
*/
constructor(name: string, number: number) {
this._name = name;
this._number = number;
}
/**
* @brief Obtient le nom de la catégorie.
* @return {string} Le nom de la catégorie.
*/
get name(): string {
return this._name;
}
/**
* @brief Obtient le nombre de la catégorie.
* @return {number} Le nombre de la catégorie.
*/
get number(): number {
return this._number;
}
/**
* @brief Modifie le nom de la catégorie.
* @param {string} name - Le nom de la categorie.
*/
set name(name: string) {
this._name = name;
}
/**
* @brief Modifie le nombre de la catégorie.
* @param {number} number - Le nombre de la catégorie.
*/
set number(number: number) {
this._number = number;
}
}

@ -0,0 +1,25 @@
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);
});
});

@ -21,7 +21,7 @@ export class CustomJoke extends Joke {
* @param {string} punchline - La chute de la blague.
* @param {string} image - L'URL de l'image associée à la blague.
*/
constructor(id: string, type: string, setup: string, punchline: string, image: string) {
constructor(id: string, type: string, setup: string, image: string, punchline: string = "") {
super(type, setup, punchline, image); // Assuming Joke class has these properties
this._id = id;
}

@ -0,0 +1,35 @@
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);
});
});

@ -96,6 +96,9 @@ export abstract class Joke {
* @return {string} Un résumé de la blague.
*/
public summary(): string {
if(this.punchline.length <= 25){
return this.punchline;
}
return this.punchline.substring(0, 24) + "...";
}
@ -104,6 +107,6 @@ export abstract class Joke {
* @return {string} Une description textuelle de la blague.
*/
public description(): string {
return this.type + this.summary();
return this.type + ", " + this.summary();
}
}

@ -0,0 +1,61 @@
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');
});
});

@ -21,11 +21,12 @@ export class SampleJoke extends Joke {
* @param {string} punchline - La chute de la blague.
* @param {string} image - L'URL de l'image associée à la blague.
*/
constructor(id: number, type: string, setup: string, punchline: string, image: string) {
constructor(id: number, type: string, setup: string, image: string, punchline: string = "") {
super(type, setup, punchline, image); // Assuming Joke class has these properties
this._id = id;
}
/**
* @brief Obtient l'identifiant de la blague d'échantillon.
* @return {number} L'identifiant de la blague d'échantillon.

@ -0,0 +1,126 @@
import {DarkTheme, DefaultTheme, NavigationContainer, Theme} from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import {Image, View} from 'react-native';
import HomeScreen from "../screens/HomeScreen";
import Favorites from "../screens/Favorites";
import Add from "../screens/AddScreen";
import Settings from "../screens/Settings";
import {darksalmonColor, greyColor, indigoColor} from "../assets/Theme";
import StackNavigation from "./StackNavigation";
import {useEffect, useState} from "react";
import {getTheme} from "../redux/thunk/ThemeThunk";
export default function NavigationBar() {
const BottomTabNavigator = createBottomTabNavigator();
const homeIcon = require("../assets/home_icon.png");
const listIcon = require("../assets/list_icon.png");
const addIcon = require("../assets/add_icon.png");
const favoriteIcon = require("../assets/favorite_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 (
<NavigationContainer theme={ themes.dark === false ? DefaultTheme : DarkTheme}>
<BottomTabNavigator.Navigator initialRouteName="Accueil"
screenOptions={{
headerStyle: {
backgroundColor: indigoColor,
},
headerTitleStyle: {
color:darksalmonColor,
fontSize:24,
textAlign: "center",
paddingBottom:20,
},
headerTitleAlign: 'center',
tabBarShowLabel: false,
tabBarStyle: {
backgroundColor: indigoColor,
}
}}
>
<BottomTabNavigator.Screen name="Accueil" component={HomeScreen}
options={{
tabBarIcon: ({focused}) => (
<Image
source={homeIcon}
style={{
width: 30, height: 30,
tintColor: focused ? darksalmonColor : greyColor,
}}
/>
)
}}/>
<BottomTabNavigator.Screen name="Catalogue" component={StackNavigation}
options={{
headerShown: false,
tabBarIcon: ({focused}) => (
<Image
source={listIcon}
style={{
width: 30, height: 30,
tintColor: focused ? darksalmonColor : greyColor,
}}
/>
)
}}/>
<BottomTabNavigator.Screen name="Ajout d'une blague" component={Add}
options={{
tabBarIcon: ({focused}) => (
<View style={{backgroundColor: greyColor, borderRadius: 5, padding: 10}}>
<Image
source={addIcon}
style={{
width: 20, height: 20,
tintColor: focused ? darksalmonColor : "black",
}}
/>
</View>
)
}}/>
<BottomTabNavigator.Screen name="Favoris" component={Favorites}
options={{
tabBarIcon: ({focused}) => (
<Image
source={favoriteIcon}
style={{
width: 30, height: 30,
tintColor: focused ? darksalmonColor : greyColor,
}}
/>
)
}}/>
<BottomTabNavigator.Screen name="Parametres" component={Settings}
options={{
tabBarIcon: ({focused}) => (
<Image
source={settingsIcon}
style={{
width: 30, height: 30,
tintColor: focused ? darksalmonColor : greyColor,
}}
/>
)
}}/>
</BottomTabNavigator.Navigator>
</NavigationContainer>
)
}

@ -0,0 +1,26 @@
import {createStackNavigator} from "@react-navigation/stack";
import Catalogue from "../screens/Catalogue";
import JokeDetailsScreen from "../screens/JokeDetailsScreen";
import {darksalmonColor, indigoColor} from "../assets/Theme";
export default function StackNavigation() {
const Stack = createStackNavigator();
return (
<Stack.Navigator initialRouteName="catalogue" screenOptions={{
headerStyle: {
backgroundColor: indigoColor,
},
headerTitleStyle: {
marginTop: 10,
color:darksalmonColor,
fontSize:24,
textAlign: "center",
paddingBottom:30,
},
headerTitleAlign: 'center'
}}>
<Stack.Screen name="catalogue" component={Catalogue} />
<Stack.Screen name="JokeDetails" component={JokeDetailsScreen}/>
</Stack.Navigator>
)
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,63 @@
{
"name": "tp-react-native",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"ts:check": "tsc",
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"test": "jest"
},
"dependencies": {
"@jest/globals": "^29.7.0",
"@react-native-async-storage/async-storage": "^1.23.1",
"@react-navigation/bottom-tabs": "^6.5.11",
"@react-navigation/native": "^6.1.10",
"@react-navigation/stack": "^6.3.21",
"@reduxjs/toolkit": "^2.2.1",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^12.4.5",
"@types/react": "~18.2.45",
"enzyme": "^3.11.0",
"expo": "~50.0.3",
"expo-status-bar": "~1.11.1",
"jest": "^29.7.0",
"jest-expo": "^50.0.4",
"react": "18.2.0",
"react-native": "0.73.2",
"react-native-gesture-handler": "^2.15.0",
"react-native-safe-area-context": "^4.9.0",
"react-redux": "^9.1.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"typescript": "^5.3.0"
},
"devDependencies": {
"@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
}

@ -0,0 +1,19 @@
import {Category} from "../../model/Category";
export enum ActionType {
FETCH_CATEGORIES = 'FETCH_CATEGORIES',
}
type actionFetch = {
type: ActionType.FETCH_CATEGORIES;
payload: Category[];
}
export type Action = actionFetch;
export const setCategoriesList = (categoriesList: Category[]) => {
return {
type: ActionType.FETCH_CATEGORIES,
payload: categoriesList,
};
}

@ -0,0 +1,23 @@
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);
});
});
});

@ -0,0 +1,53 @@
import {CustomJoke} from "../../model/CustomJoke";
import {SampleJoke} from "../../model/SampleJoke";
export enum ActionType {
FETCH_CUSTOM_JOKES = 'FETCH_CUSTOM_JOKES',
POST_CUSTOM_JOKE = "POST_CUSTOM_JOKE",
FETCH_JOKES_BY_ID = "FETCH_JOKES_BY_ID",
DELETE_CUSTOM_JOKE = "DELETE_CUSTOM_JOKE"
}
type actionPostFetch = {
type: ActionType.POST_CUSTOM_JOKE;
payload: CustomJoke;
}
type actionGetFetch = {
type: ActionType.FETCH_CUSTOM_JOKES;
payload: CustomJoke[];
}
type actionGetByFetch = {
type: ActionType.FETCH_JOKES_BY_ID;
payload: CustomJoke;
}
type actionDeleteFetch = {
type: ActionType.DELETE_CUSTOM_JOKE;
payload: string;
}
export type Action = actionPostFetch | actionGetFetch | actionGetByFetch | actionDeleteFetch;
export const setPostJoke = (customJoke: CustomJoke) => {
return {
type: ActionType.POST_CUSTOM_JOKE,
payload: customJoke
}
}
export const setCustomJokesList = (customJokesList: CustomJoke[]) => {
return {
type: ActionType.FETCH_CUSTOM_JOKES,
payload: customJokesList
};
}
export const setCustomJokeById = (customJoke: CustomJoke) => {
return {
type: ActionType.FETCH_JOKES_BY_ID,
payload: customJoke,
};
}
export const setDeleteJoke = (jokeId: string) => {
return {
type: ActionType.DELETE_CUSTOM_JOKE,
payload: jokeId
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save