Compare commits
35 Commits
@ -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
|
@ -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 |
After Width: | Height: | Size: 604 B |
After Width: | Height: | Size: 209 B |
After Width: | Height: | Size: 315 B |
After Width: | Height: | Size: 603 B |
After Width: | Height: | Size: 744 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 502 B |
After Width: | Height: | Size: 705 B |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 301 B |
After Width: | Height: | Size: 265 B |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 805 B |
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
|
||||
}
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
@ -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>
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|