Compare commits

...

35 Commits

Author SHA1 Message Date
Tony Fages 5aa949ae0f Mise à jour de 'README.md'
continuous-integration/drone/push Build is passing Details
1 year ago
Tony Fages c905cbb7fa add tests
continuous-integration/drone/push Build is passing Details
1 year ago
Tony Fages fe5d58285e first tests
continuous-integration/drone/push Build is passing Details
1 year ago
Tony Fages d187888198 first tests
continuous-integration/drone/push Build is passing Details
1 year ago
Tony Fages a20e3d13af Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is passing Details
1 year ago
Tony Fages 99d6e6cdac Mise à jour de '.drone.yml'
continuous-integration/drone/push Build encountered an error Details
1 year ago
Tony Fages 5ec03de121 Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is failing Details
1 year ago
Tony Fages 5bce0b3d7e Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is failing Details
1 year ago
Tony Fages 9f6e3c1a07 Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is failing Details
1 year ago
Tony Fages d169c5aa8c Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is failing Details
1 year ago
Tony Fages a9cba28673 Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is failing Details
1 year ago
Tony Fages 4a89f463ca Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is failing Details
1 year ago
Tony Fages 7e24172efe Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is passing Details
1 year ago
Tony Fages f6f0f142e9 Drone
continuous-integration/drone/push Build is passing Details
1 year ago
Tony Fages 4fc7b3ddb5 Drone
continuous-integration/drone/push Build is failing Details
1 year ago
Tony Fages e53ea4f06b Drone
continuous-integration/drone/push Build encountered an error Details
1 year ago
Tony Fages a83661eb5e Drone
continuous-integration/drone/push Build encountered an error Details
1 year ago
Tony Fages 2383e13a6c Drone
continuous-integration/drone Build encountered an error Details
1 year ago
Tony Fages 2ff3cb5d34 Drone
1 year ago
Tony Fages da7fe0d80d Merge branch 'Tp8'
1 year ago
Tony Fages 7220483540 tp8
1 year ago
Tony Fages 2dee48564e tp8
1 year ago
Tony Fages 51ceb3511f tp8
1 year ago
Tony Fages 296c8d66e8 addFav
1 year ago
Tony Fages a525cb40e7 tp8
1 year ago
Tony Fages 747f190096 tp8
1 year ago
Tony Fages 194f68e83c Merge remote-tracking branch 'origin/tp7'
1 year ago
Tony Fages 8156251771 Merge remote-tracking branch 'origin/tp6'
1 year ago
Tony Fages 0036ac877b Merge remote-tracking branch 'origin/part5'
1 year ago
Tony Fages 6da31b6cbc tp8
1 year ago
Tony Fages 9bbb4d508b tp7
1 year ago
Tony Fages f09a8fcc8f finission tp
1 year ago
Tony Fages ecd70bd7e9 passurdupush
1 year ago
Tony Fages dd8de95ed6 tp6
1 year ago
Tony Fages 02cbc2a196 ajout addPage + Post Joke method
1 year ago

@ -0,0 +1,22 @@
kind: pipeline
type: docker
name: default
steps:
- name: build
image: hub.codefirst.iut.uca.fr/camille.petitalot/drone-sonarplugin-reactnative:latest
commands:
- cd JokesApp
- npm install
- name : sonar
image : hub.codefirst.iut.uca.fr/camille.petitalot/drone-sonarplugin-reactnative:latest
environment:
sonar_host: https://codefirst.iut.uca.fr/sonar/
sonar_token:
from_secret: SECRET_SONAR_LOGIN
project_key: JokesAppSonar
commands:
- cd JokesApp
- npm install
- npm run test
- sonar-scanner -Dsonar.projectKey=$${project_key} -Dsonar.sources=. -Dsonar.host.url=$${sonar_host} -Dsonar.login=$${sonar_token} -Dsonar.javascript.lcov.reportPaths=/lcov.info -Dsonar.exclusions=/coverage//*,/tests/*/

4
.gitignore vendored

@ -419,7 +419,8 @@ FodyWeavers.xsd
.LSOverride
# Icon must end with two \r
Icon
Icon
# Thumbnails
._*
@ -440,3 +441,4 @@ Network Trash Folder
Temporary Items
.apdisk
coverage

5
.idea/.gitignore vendored

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Tp_ReactNative.iml" filepath="$PROJECT_DIR$/.idea/Tp_ReactNative.iml" />
</modules>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

@ -3,3 +3,5 @@
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# GitHub Copilot persisted chat sessions
/copilot/chatSessions

@ -5,9 +5,11 @@ import {Navigation} from "./navigation/Navigation";
import {darksalmonColor, indigo, purpleColor} from "./Theme";
import {Provider} from "react-redux";
import store from "./redux/store";
import AsyncStorage from "@react-native-async-storage/async-storage";
export default function App() {
//AsyncStorage.clear()
return (
<Provider store={store}>
<SafeAreaView style={styles.container}>

@ -1,5 +1,75 @@
import {DarkTheme, Theme, useTheme} from "@react-navigation/native";
import React, {useEffect, useState} from "react";
import DefaultTheme from "@react-navigation/native/src/theming/DefaultTheme";
import {useColorScheme} from "react-native";
export const indigo = "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 whiteColor = "rgba(239, 239, 253, 1)";
export const blackColor = "rgba(0, 0, 0, 1)";
export default function usePersonalTheme() {
useTheme()
const theme = React.useContext(ThemeContext);
const [isDark, setIsDark] = useState(false);
if (isDark) {
theme.dark = true;
theme.colors = MyDarkTheme.colors;
}
else {
theme.dark = false;
theme.colors = MyLightTheme.colors;
}
return theme;
}
//const isDark = false;
const MyDarkTheme: Theme = {
dark: true,
colors: {
primary: indigo,
background: purpleColor,
card: darksalmonColor,
text: whiteColor,
border: whiteColor,
notification: whiteColor,
},
};
const MyLightTheme: Theme = {
dark: false,
colors: {
primary: whiteColor,
background: greyColor,
card: '',
text: blackColor,
border: blackColor,
notification: blackColor,
},
};
const ThemeContext = React.createContext<Theme>(MyDarkTheme);

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

@ -3,22 +3,37 @@ import {darksalmonColor, greyColor, indigo, purpleColor, whiteColor} from "../Th
import React, {useState} from "react";
import {SampleJoke} from "../model/SampleJoke";
import {Joke} from "../model/Joke";
import {CustomJoke} from "../model/CustomJoke";
import {removeFavoriteJoke, storeFavoriteJoke, useAppDispatch, useAppSelector} from "../redux/store";
type DetailJokeProps = {
item: SampleJoke;
item: CustomJoke;
}
export function DetailJoke(props: DetailJokeProps) {
const [isActivated, setIsActivated] = useState(false);
const favoriteJokes = useAppSelector(state => state.customReducer.favoriteJokes) as [CustomJoke, SampleJoke];
const dispatch = useAppDispatch();
const isFav : boolean = favoriteJokes.some(it => it.id == props.item.id )
const [isActivated, setIsActivated] = useState(isFav);
const [isActivated2, setIsActivated2] = useState(false);
function toggleActivation() {
setIsActivated(!isActivated);
if (isActivated) {
console.log("Joke retirée des favoris");
dispatch(removeFavoriteJoke(props.item));
}
else {
dispatch(storeFavoriteJoke(props.item));
console.log("Joke ajoutée aux favoris");
}
}
function toggleActivation2() {
setIsActivated2(!isActivated2);
}

@ -35,4 +35,11 @@ export class JokeFactory {
// array.push(new SampleJoke(joke.type, joke.setup, joke.image,joke.punchline, joke.id));
//})
}
static createCustomJokeById(jsonArray: string): CustomJoke {
let array = [];
let json = JSON.parse(jsonArray);
return new CustomJoke(json.type, json.setup, json.punchline,json.image, json.id)
}
}

@ -1,37 +1,53 @@
import React from "react";
import {NavigationContainer} from "@react-navigation/native";
import React, {useContext, useEffect, useState} from "react";
import {DarkTheme, DefaultTheme, NavigationContainer, Theme, useTheme} from "@react-navigation/native";
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
import {ListJokeScreen} from "../screens/ListJokeScreen";
import {Image, StyleSheet, View} from "react-native";
import {darksalmonColor, greyColor, indigo, purpleColor} from "../Theme";
import {Image, StyleSheet, useColorScheme, View} from "react-native";
import usePersonalTheme, {darksalmonColor, greyColor, indigo, purpleColor} from "../Theme";
import {AccueilScreen} from "../screens/AccueilScreen";
import {AddJokeScreen} from "../screens/AddJokeScreen";
import {SettingsScreen} from "../screens/SettingsScreen";
import {JokeListItems} from "../components/ListeJokeComponent";
import StackNavigation from "./StackNavigation";
import {CatalogueScreen, FavoriteScreen} from "./StackNavigation";
const homeIcon = require("../assets/home_icon.png");
const listIcon = require("../assets/list_icon.png");
const addIcon = require("../assets/add_icon.png");
const favIcon = require("../assets/favorite_icon.png");
const setIcon = require("../assets/settings_icon.png");
import store, {getTheme, storeTheme} from "../redux/store";
export function Navigation(){
const BottomTabNavigator = createBottomTabNavigator();
const [themes, setThemes] = useState<Theme>(DefaultTheme);
useEffect(() => {
const fetchTheme = async () => {
const theme = await getTheme();
setThemes(theme);
};
fetchTheme();
});
if (themes == null) {
return null;
}
return (
<NavigationContainer>
<BottomTabNavigator.Navigator initialRouteName="Home" screenOptions={{
<NavigationContainer theme={ themes.dark === false ? DefaultTheme : DarkTheme} >
<BottomTabNavigator.Navigator initialRouteName="Home" screenOptions={{
headerTitleStyle: {
fontSize: 24,
fontWeight: 'bold',
color: darksalmonColor,
color: themes.colors.text,
},
headerStyle: {
backgroundColor: indigo,
},
tabBarShowLabel: false,
tabBarStyle: styles.top,
}} >
}}>
<BottomTabNavigator.Screen name="Accueil" component={AccueilScreen}
options={{
tabBarIcon: ({focused}) => (
@ -41,7 +57,7 @@ export function Navigation(){
/>
)
}}/>
<BottomTabNavigator.Screen name="Catalogue" component={StackNavigation}
<BottomTabNavigator.Screen name="Catalogue" component={CatalogueScreen}
options={{
tabBarIcon: ({focused}) => (
<Image source={listIcon}
@ -62,13 +78,14 @@ export function Navigation(){
}}/>
<BottomTabNavigator.Screen name="Favoris" component={ListJokeScreen}
<BottomTabNavigator.Screen name="Favoris" component={FavoriteScreen}
options={{
tabBarIcon: ({focused}) => (
<Image source={favIcon}
style={{ tintColor: focused ? darksalmonColor : purpleColor }}
/>
)
),
headerShown: false,
}}/>
<BottomTabNavigator.Screen name="Paramètres" component={SettingsScreen}
options={{
@ -108,3 +125,30 @@ const styles = StyleSheet.create({
});
const stylesDark = StyleSheet.create({
title: {
fontSize: 24,
color: 'darksalmon',
textAlign: 'center',
fontWeight: 'bold',
marginVertical: 20,
},
top: {
backgroundColor : indigo
},
addJoke: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: greyColor,
width: '70%',
height: '100%',
borderRadius: 4,
marginTop: 4,
},
});

@ -6,12 +6,40 @@ import {DetailJoke} from "../components/DetailJoke";
import JokeDetailScreen from "../screens/JokeDetailScreen";
import {ListJokeScreen} from "../screens/ListJokeScreen";
import {darksalmonColor, indigo, purpleColor} from "../Theme";
import {ListFavoriteJokeScreen} from "../screens/ListFavoriteJokeScreen";
export default function StackNavigation() {
export function CatalogueScreen(){
const Stack = createStackNavigator();
return(
<Stack.Navigator initialRouteName="CatalogueStack" screenOptions={
{
headerTitleStyle: {
fontSize: 24,
fontWeight: 'bold',
color: darksalmonColor,
},
headerStyle: {
backgroundColor: indigo,
},
headerTitle : "Catalogue",
headerBackTitleVisible: false,
headerTintColor: darksalmonColor,
}
}>
<Stack.Screen name="CatalogueStack" component={ListJokeScreen} options={{ headerTitle: 'Catalogue' }}/>
<Stack.Screen name="JokeDetail" component={JokeDetailScreen} options={{ headerTitle: 'Detail d une blague' }
} />
</Stack.Navigator>
)}
export function FavoriteScreen(){
const Stack = createStackNavigator();
return(
<Stack.Navigator initialRouteName="CatalogueStack" screenOptions={
<Stack.Navigator initialRouteName="FavoriteStack" screenOptions={
{
headerTitleStyle: {
fontSize: 24,
@ -23,15 +51,15 @@ export default function StackNavigation() {
backgroundColor: indigo,
},
headerTitle : "Catalogue",
headerTitle : "Favoris",
headerBackTitleVisible: false,
headerTintColor: darksalmonColor,
}
}>
<Stack.Screen name="CatalogueStack" component={ListJokeScreen} options={{ headerTitle: 'Catalogue' }}/>
<Stack.Screen name="JokeDetail" component={JokeDetailScreen} options={{ headerTitle: 'Detail d une blague' }
} />
<Stack.Screen name="FavoriteStack" component={ListFavoriteJokeScreen} options={{ headerTitle: 'Favoris' }}/>
<Stack.Screen name="JokeDetail" component={JokeDetailScreen} options={{ headerTitle: 'Detail d une blague' }
} />
</Stack.Navigator>
)
}

File diff suppressed because it is too large Load Diff

@ -6,27 +6,52 @@
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
"web": "expo start --web",
"test": "jest --coverage "
},
"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",
"**.test.ts",
"**.test.tsx"
],
"testEnvironment": "node",
"testEnvironmentOptions": {
"browsers": [
"chrome",
"firefox",
"safari"
]
}
},
"dependencies": {
"@expo/ngrok": "^2.5.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",
"@types/react": "~18.2.45",
"expo": "~50.0.3",
"expo": "^50.0.14",
"expo-status-bar": "~1.11.1",
"react": "18.2.0",
"react-native": "0.73.2",
"react-native": "^0.73.2",
"react-native-gesture-handler": "^2.15.0",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-redux": "^9.1.0",
"redux": "^5.0.1",
"typescript": "^5.3.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@expo/ngrok": "^4.1.0"
"@expo/ngrok": "^4.1.0",
"jest-expo": "^50.0.4"
},
"private": true
}

@ -0,0 +1,138 @@
import {CustomJoke} from "../../model/CustomJoke";
import {JokeFactory} from "../../model/JokeFactory";
export enum CustomActionType {
POST_CUSTOM_JOKE = 'POST_CUSTOM_JOKE',
FETCH_CUSTOMS_JOKE = 'FETCH_CUSTOMS_JOKE',
FETCH_CUSTOMS_JOKE_BY_ID = 'FETCH_CUSTOMS_JOKE_BY_ID',
DELETE_CUSTOM_JOKE = 'DELETE_CUSTOM_JOKE',
FETCH_FAVORITE_JOKE = 'FETCH_FAVORITE_JOKE'
}
export interface CustomAction {
type: CustomActionType;
payload: [];
}
export interface postCustomAction {
type: CustomActionType;
payload: CustomJoke;
}
export interface CustomsAction {
type: CustomActionType;
payload: CustomJoke[];
}
export type Action = CustomAction;
export const setPostJoke = (postJoke: CustomJoke): postCustomAction => {
return {
type: CustomActionType.POST_CUSTOM_JOKE,
payload: postJoke
}
}
export const setCustomsJoke = (customJokes: CustomJoke[]): CustomsAction => {
return {
type: CustomActionType.FETCH_CUSTOMS_JOKE,
payload: customJokes
}
}
export const setCustomsJokeById = (completCustomJoke: CustomJoke): postCustomAction => {
return {
type: CustomActionType.FETCH_CUSTOMS_JOKE_BY_ID,
payload: completCustomJoke
}
}
export const setDeleteCustomJoke = (deleteCustomJoke: CustomJoke): postCustomAction => {
return {
type: CustomActionType.DELETE_CUSTOM_JOKE,
payload: deleteCustomJoke
}
}
export const setFavoriteJoke = (favoriteJokes: CustomJoke[]): CustomsAction => {
return {
type: CustomActionType.FETCH_FAVORITE_JOKE,
payload: favoriteJokes
}
}
export const postJoke = (type : string, setup : string, punchline : string) => {
return async dispatch => {
try {
console.log('type', type, 'setup', setup, 'punchline', punchline);
const reponse = await fetch('https://iut-weather-api.azurewebsites.net/jokes/', {
method: 'POST',
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(
{
type: type,
setup: setup,
punchline: punchline
}
)
});
const data = await reponse.json();
dispatch(setPostJoke(data));
} catch (error) {
console.log('Error---------', error);
}
}
}
export const getJokesCustoms = () => {
return async dispatch => {
try {
const custom = await fetch('https://iut-weather-api.azurewebsites.net/jokes/');
const customsJson = await custom.text();
const customJokes = JokeFactory.createCustomJokes(customsJson);
dispatch(setCustomsJoke(customJokes));
} catch (error) {
console.log('Error---------', error);
}
}
}
export const getJokesCustomsById = (id : number) => {
return async dispatch => {
try {
const custom = await fetch('https://iut-weather-api.azurewebsites.net/jokes/' + id);
const customsJson = await custom.text();
const customJokes = JokeFactory.createCustomJokeById(customsJson);
console.log('customJokes', customJokes);
dispatch(setCustomsJokeById(customJokes));
} catch (error) {
console.log('Error---------', error);
}
}
}
export const deleteJoke = (id : number) => {
return async dispatch => {
try {
const custom = await fetch('https://iut-weather-api.azurewebsites.net/jokes/' + id, {
method: 'DELETE',
headers: {
Accept: "application/json",
"Content-Type": "application/json",
}
});
console.log('Suppression');
dispatch(setDeleteCustomJoke({} as CustomJoke));
} catch (error) {
console.log('Error---------', error);
}
}
}

@ -15,12 +15,14 @@ export interface SampleAction {
export interface SampleActionComplet {
type: SampleActionType;
payload: SampleJoke;
}
export type Action = SampleAction;
export const setSample = (sample: SampleJoke[]): SampleAction => {
return {
type: SampleActionType.FETCH_SAMPLE,
@ -42,6 +44,9 @@ export const setCompletJokes = (completJoke: SampleJoke): SampleActionComplet =>
}
}
export const getSampleJoke = () => {
return async dispatch => {
try {
@ -79,4 +84,4 @@ export const getCompletJokes = (id : number) => {
console.log('Error---------', error);
}
}
}
}

@ -0,0 +1,56 @@
import {SampleJoke} from "../../model/SampleJoke";
import {CustomJoke} from "../../model/CustomJoke";
import {CustomActionType} from "../actions/customAction";
interface state {
postJoke: CustomJoke;
customJokes: CustomJoke[];
completCustomJoke: CustomJoke;
deleteCustomJoke: CustomJoke;
favoriteJokes: CustomJoke[];
}
// initial state for sampleJokes
const initialState: state = {
postJoke: {} as CustomJoke,
customJokes: [],
completCustomJoke: {} as CustomJoke,
deleteCustomJoke: {} as CustomJoke,
favoriteJokes: []
}
// app reducer for sampleJokes
// @ts-ignore
export default appReducer = (state = initialState, action: Action) => {
switch (action.type) {
case CustomActionType.POST_CUSTOM_JOKE:
return {
...state,
postJoke: action.payload,
}
case CustomActionType.FETCH_CUSTOMS_JOKE:
return {
...state,
customJokes: action.payload,
}
case CustomActionType.FETCH_CUSTOMS_JOKE_BY_ID:
return {
...state,
completCustomJoke: action.payload,
}
case CustomActionType.DELETE_CUSTOM_JOKE:
return {
...state,
deleteCustomJoke: action.payload,
}
case CustomActionType.FETCH_FAVORITE_JOKE:
return {
...state,
favoriteJokes: action.payload,
}
default:
return state;
}
}

@ -1,5 +1,6 @@
import {SampleJoke} from "../../model/SampleJoke";
import {Action, SampleActionType} from "../actions/sampleAction";
import {CustomJoke} from "../../model/CustomJoke";
interface state {
@ -12,7 +13,7 @@ interface state {
const initialState: state = {
completJoke: {} as SampleJoke,
sampleJoke: [],
recentJokes: []
recentJokes: [],
}
// app reducer for sampleJokes
@ -35,6 +36,7 @@ export default appReducer = (state = initialState, action: Action) => {
completJoke: action.payload,
}
default:
return state;
}

@ -1,14 +1,23 @@
import { configureStore } from '@reduxjs/toolkit';
import categorieReducer from './reducers/categoryReducer';
import sampleReducer from './reducers/sampleJokeReducer';
import customReducer from "./reducers/customJokeReducer";
import AsyncStorage from '@react-native-async-storage/async-storage';
import {DefaultTheme, Theme} from "@react-navigation/native";
import {CustomJoke} from "../model/CustomJoke";
import {setFavoriteJoke} from "./actions/customAction";
import {TypedUseSelectorHook, useDispatch, useSelector} from "react-redux";
import {SampleJoke} from "../model/SampleJoke";
import {Joke} from "../model/Joke";
import {JokeFactory} from "../model/JokeFactory";
const reducer = {
categorieReducer: categorieReducer,
sampleReducer: sampleReducer
sampleReducer: sampleReducer,
customReducer: customReducer
};
// @ts-ignore
const store = configureStore({
// @ts-ignore
reducer,
@ -18,4 +27,76 @@ const store = configureStore({
})
});
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 : DefaultTheme;
} catch(e) {
console.log(e);
}
}
export const storeFavoriteJoke = (joke : CustomJoke) => {
return async dispatch => {
try {
const favoriteJokes = await AsyncStorage.getItem('favorites');
const favoriteJokesList: CustomJoke[] = favoriteJokes != null ? JokeFactory.createCustomJokes(favoriteJokes) : [];
favoriteJokesList.push(joke);
await AsyncStorage.setItem('favorites', JSON.stringify(favoriteJokesList.map(j => ({type: j.type, setup: j.setup, punchline: j.punchline, image: j.image, id: j.id}))));
dispatch(setFavoriteJoke(favoriteJokesList));
} catch (e) {
console.log(e);
}
}
}
export const getFavorite = () => {
return async (dispatch) => {
try {
const favoriteJokes = await AsyncStorage.getItem('favorites');
let favoriteJokesList = favoriteJokes != null ? JSON.parse(favoriteJokes) : [];
favoriteJokesList = favoriteJokesList.filter(joke => joke.id !== undefined);
await AsyncStorage.setItem('favorites', JSON.stringify(favoriteJokesList));
const favorites = favoriteJokesList.map(joke => new SampleJoke(joke["type"], joke["setup"], joke["punchline"], joke["image"], joke["id"]))
dispatch(setFavoriteJoke(favorites));
} catch (e) {
console.log(e);
}
}
}
export const removeFavoriteJoke = (joke : CustomJoke) => {
return async dispatch => {
try {
const favoriteJokes = await AsyncStorage.getItem('favorites');
let favoriteJokesList: CustomJoke[] = favoriteJokes != null ? JokeFactory.createCustomJokes(favoriteJokes) : [];
const index = favoriteJokesList.findIndex((j) => j.id === joke.id);
if (index !== -1) {
favoriteJokesList.splice(index, 1);
await AsyncStorage.setItem('favorites', JSON.stringify(favoriteJokesList.map(j => ({type: j.type, setup: j.setup, punchline: j.punchline, image: j.image, id: j.id}))));
dispatch(setFavoriteJoke(favoriteJokesList));
}
} catch (e) {
console.log("An error occurred", e);
}
}
}
export type AppDispatch = typeof store.dispatch;
export type AppStore = ReturnType<typeof store.getState>;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<AppStore> = useSelector;
export default store;

@ -1,33 +1,169 @@
import React from "react";
import {FlatList, Image, SafeAreaView, ScrollView, SectionListComponent, StyleSheet, Text, View} from "react-native";
import {indigo, purpleColor} from "../Theme";
import React, { useEffect, useState } from "react";
import { Button, StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native";
import { darksalmonColor, indigo, purpleColor, whiteColor } from "../Theme";
import { useDispatch, useSelector } from "react-redux";
import { postJoke} from "../redux/actions/customAction";
export function AddJokeScreen() {
const [joke, setJoke] = useState("");
const [jokeFall, setJokeFall] = useState("");
const [category, setCategory] = useState("");
const [categoryExceeded, setCategoryExceeded] = useState(false);
const [buttonDisabled, setButtonDisabled] = useState(true);
const MAX_CATEGORY_LENGTH = 10;
const dispatch = useDispatch();
useEffect(() => {
if (joke !== "" && jokeFall !== "" && category !== "") {
setButtonDisabled(false);
} else {
setButtonDisabled(true);
}
}, [joke, jokeFall, category]);
const postjoke = async () => {
// @ts-ignore
await dispatch(postJoke(joke, jokeFall, category));
clearFields();
};
const clearFields = () => {
setJoke("");
setJokeFall("");
setCategory("");
setCategoryExceeded(false);
setButtonDisabled(true);
};
const handleCategoryChange = (text) => {
if (text.length > MAX_CATEGORY_LENGTH) {
setCategoryExceeded(true);
} else {
setCategoryExceeded(false);
setCategory(text);
}
};
return (
<View style={styles.font}>
<Text style={styles.text}>Ajouter une blague</Text>
<Text style={styles.text}>In Work</Text>
</View>
);
}
<Text style={styles.titleTextInput}>Blague</Text>
<TextInput
editable
multiline
style={styles.textInput}
value={joke}
onChangeText={(text) => setJoke(text)}
/>
<Text style={styles.titleTextInput}>Chute de la blague</Text>
<TextInput
editable
multiline
style={styles.textInput}
value={jokeFall}
onChangeText={(text) => setJokeFall(text)}
/>
<Text style={styles.titleTextInput}>Catégorie</Text>
<TextInput
editable
multiline
style={styles.textInput1}
value={category}
onChangeText={handleCategoryChange}
></TextInput>
<View style={styles.characterCountContainer}>
<Text style={styles.characterCountText}>
{category.length}/{MAX_CATEGORY_LENGTH}
</Text>
</View>
<TouchableOpacity
style={[styles.viewButtonCreate, { opacity: buttonDisabled ? 0.5 : 1 }]}
disabled={buttonDisabled} // Désactiver le bouton si buttonDisabled est vrai
onPress={postjoke}
>
<Text style={styles.textButton1}>CRÉER</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.viewButtonClear} onPress={clearFields}>
<Text style={styles.textButton2}>EFFACER</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
font: {
backgroundColor: purpleColor,
width: "100%",
height: "100%",
},
titleTextInput: {
fontSize: 20,
color: whiteColor,
textAlign: "left",
fontWeight: "bold",
marginTop: 25,
marginVertical: 10,
marginLeft: 10,
},
textInput: {
backgroundColor: indigo,
width: '100%',
height: '100%',
height: "13%",
color: whiteColor,
textAlign: "left",
textAlignVertical: "top",
margin: 10,
padding: 30,
paddingTop: 15,
},
textInput1: {
backgroundColor: indigo,
height: "9%",
color: whiteColor,
textAlign: "left",
textAlignVertical: "top",
margin: 10,
padding: 20,
paddingTop: 10,
fontSize: 12,
},
viewButtonCreate: {
backgroundColor: darksalmonColor,
height: "7%",
marginLeft: 10,
marginRight: 10,
marginTop: 20,
borderRadius: 10,
},
viewButtonClear: {
backgroundColor: whiteColor,
height: "7%",
marginLeft: 10,
marginRight: 10,
marginTop: 20,
borderRadius: 10,
},
textButton1: {
fontSize: 16,
color: whiteColor,
alignSelf: "center",
padding: 12,
fontWeight: "bold",
},
textButton2: {
fontSize: 16,
color: darksalmonColor,
alignSelf: "center",
padding: 12,
fontWeight: "bold",
},
characterCountContainer: {
alignItems: 'flex-end',
bottom: 10,
right: 10,
},
characterCountText: {
fontSize: 12,
color: whiteColor,
},
text: {
fontSize: 24,
color: 'darksalmon',
textAlign: 'center',
fontWeight: 'bold',
marginVertical: 20,
}
});

@ -1,4 +1,4 @@
import {View, Text, StyleSheet} from "react-native";
import {View, Text, StyleSheet, TouchableOpacity, Image} from "react-native";
import {indigo, purpleColor, whiteColor} from "../Theme";
import React, {useEffect} from "react";
import {CustomJoke} from "../model/CustomJoke";
@ -6,26 +6,42 @@ import {DetailJoke} from "../components/DetailJoke";
import {Joke} from "../model/Joke";
import {useDispatch, useSelector} from "react-redux";
import {getCompletJokes, setCompletJokes, setSample} from "../redux/actions/sampleAction";
import {validatePathConfig} from "@react-navigation/native";
//svjh
import {useIsFocused, useNavigation, validatePathConfig} from "@react-navigation/native";
import {deleteJoke, getJokesCustomsById} from "../redux/actions/customAction";
import {Navigation} from "../navigation/Navigation";
export default function JokeDetailScreen({route}) {
const navigation = useNavigation();
const isFocused = useIsFocused();
const dispatch = useDispatch();
const jokeId = route.params.joke;
console.log(jokeId);
const DataGen = useSelector((state: any) => state.sampleReducer.completJoke);
const isFavoris = route.params.isFavoris
const DataGen = typeof jokeId === "string" ? useSelector((state: any) => state.customReducer.completCustomJoke) : useSelector((state: any) => state.sampleReducer.completJoke);
// const DataGen = useSelector((state: any) => state.sampleReducer.completJoke);
const dispatch = useDispatch();
useEffect(() => {
const getDetails = async () => {
// @ts-ignore
await dispatch(getCompletJokes(jokeId))
{ typeof jokeId === "string" ? await dispatch(getJokesCustomsById(jokeId)) : await dispatch(getCompletJokes(jokeId));}
};
getDetails();
}, [dispatch]);
console.log(DataGen);
async function deleteJokes() {
// @ts-ignore
await dispatch(deleteJoke(jokeId));
navigation.goBack();
}
return (
<View style={styles.font}>
<DetailJoke item={DataGen}/>
{typeof jokeId === "string" ? <TouchableOpacity onPress={deleteJokes}>
<Image style={styles.img} source={require('../assets/delete-icon.png')} />
</TouchableOpacity> : null}
</View>
);
}
@ -43,5 +59,11 @@ const styles = StyleSheet.create({
textAlign: 'center',
fontWeight: 'bold',
marginVertical: 20,
},
img: {
width: 50,
height: 50,
margin: 20,
alignSelf: 'center'
}
});

@ -0,0 +1,103 @@
import {useTheme} from "@react-navigation/native";
import {darksalmonColor, indigo, whiteColor} from "../Theme";
import React, {useEffect, useState} from "react";
import {getFavorite} from "../redux/store";
import {FlatList, SafeAreaView, StyleSheet, TouchableHighlight, View, Text} from "react-native";
import {JokeListItems} from "../components/ListeJokeComponent";
import {CustomJoke} from "../model/CustomJoke";
import {SampleJoke} from "../model/SampleJoke";
import {useAppSelector, useAppDispatch} from "../redux/store";
import {useDispatch} from "react-redux";
export function ListFavoriteJokeScreen({route, navigation}){
const favoriteJokes: CustomJoke[] = useAppSelector((state) => state.customReducer.favoriteJokes);
const dispatch = useDispatch();
useEffect(() => {
const getFavoriteJokes = async () => {
// @ts-ignore
await dispatch(getFavorite());
}
getFavoriteJokes();
}, []);
const styles = themeSettings();
return (
<SafeAreaView style={styles.container}>
{ favoriteJokes.length ? (
<FlatList
data = {favoriteJokes}
renderItem={({ item }) => (
<TouchableHighlight onPress={() => navigation.navigate("JokeDetail", {"joke" : item.id})}>
<JokeListItems item={item}/>
</TouchableHighlight>
)}
keyExtractor={(item) => item.id.toString()}
/>
) : (
<Text style={styles.title}>Aucun favoris</Text>
)}
</SafeAreaView>
);
}
export function themeSettings() {
const {colors} = useTheme();
const styles = StyleSheet.create({
title: {
fontSize: 24,
color: colors.text,
textAlign: 'center',
fontWeight: 'bold',
marginVertical: 20,
},
titleResume: {
fontSize: 15,
fontWeight: 'bold',
marginBottom: 20,
},
container: {
flex: 1,
backgroundColor: colors.background,
},
top: {
backgroundColor : indigo,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
margin: 9,
},
headerText: {
fontSize: 18,
color: whiteColor,
textAlign: 'center',
fontWeight: 'bold',
},
img2: {
tintColor: whiteColor,
justifyContent: "center",
alignItems: "center",
},
button2:{
flexDirection: "row",
justifyContent: "center",
marginRight: 10,
borderRadius: 10,
alignItems: "center",
height: 30,
width: 70,
borderColor: darksalmonColor,
borderWidth: 1,
backgroundColor: darksalmonColor,
},
});
return styles;
}

@ -1,25 +1,62 @@
import {FlatList, SafeAreaView, StyleSheet, Text, TouchableHighlight, View} from "react-native";
import React, {useEffect} from "react";
import {FlatList, Image, SafeAreaView, StyleSheet, Text, TouchableHighlight, TouchableOpacity, View} from "react-native";
import React, {useContext, useEffect, useState} from "react";
import {JokeListItems} from "../components/ListeJokeComponent";
import {indigo, purpleColor} from "../Theme";
import {darksalmonColor, greyColor, indigo, purpleColor, whiteColor} from "../Theme";
import {CustomJoke} from "../model/CustomJoke";
import {useDispatch, useSelector} from 'react-redux';
import {getSampleJoke, setSample} from "../redux/actions/sampleAction";
import {getJokesCustoms} from "../redux/actions/customAction";
import {useIsFocused} from "@react-navigation/native";
export function ListJokeScreen({route, navigation}) {
const [isActivated2, setIsActivated2] = useState(false);
const isFocused = useIsFocused();
const [styles, setStyles] = useState(stylesLight);
function toggleActivation2() {
setIsActivated2(!isActivated2);
}
const DataGen = useSelector((state: any) => state.sampleReducer.sampleJoke);
const DataCustomsJoke = useSelector((state : any) => state.customReducer.customJokes)
const dispatch = useDispatch();
const getCustoms = async () => {
// @ts-ignore
await dispatch(getJokesCustoms());
}
const getSamples = async () => {
// @ts-ignore
await dispatch(getSampleJoke());
}
useEffect(() => {
const getJokes = async () => {
// @ts-ignore
await dispatch(getSampleJoke());
};
getJokes();
}, [dispatch]);
if (isFocused) {
if (isActivated2) {
getCustoms();
} else {
getSamples();
}
}
}, [isFocused, isActivated2]);
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerText}>Afficher les exemples</Text>
<TouchableOpacity style={styles.button2} onPress={toggleActivation2}>
<Image source={isActivated2 ? require('../assets/eye_icon.png') : require('../assets/eye_off_icon.png')} style={styles.img2} />
</TouchableOpacity>
</View>
<FlatList
data={DataGen}
data={isActivated2 ? DataCustomsJoke : DataGen}
renderItem={({ item }) => (
<TouchableHighlight onPress={() => navigation.navigate("JokeDetail", {"joke" : item.id})}>
<JokeListItems item={item}/>
@ -30,8 +67,7 @@ export function ListJokeScreen({route, navigation}) {
</SafeAreaView>
);
}
const styles = StyleSheet.create({
const stylesDark = StyleSheet.create({
title: {
fontSize: 24,
@ -53,4 +89,92 @@ const styles = StyleSheet.create({
top: {
backgroundColor : indigo,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
margin: 9,
},
headerText: {
fontSize: 18,
color: whiteColor,
textAlign: 'center',
fontWeight: 'bold',
},
img2: {
tintColor: whiteColor,
justifyContent: "center",
alignItems: "center",
},
button2:{
flexDirection: "row",
justifyContent: "center",
marginRight: 10,
borderRadius: 10,
alignItems: "center",
height: 30,
width: 70,
borderColor: darksalmonColor,
borderWidth: 1,
backgroundColor: darksalmonColor,
},
});
const stylesLight = StyleSheet.create({
title: {
fontSize: 24,
color: darksalmonColor,
textAlign: 'center',
fontWeight: 'bold',
marginVertical: 20,
},
titleResume: {
fontSize: 15,
fontWeight: 'bold',
marginBottom: 20,
},
container: {
flex: 1,
backgroundColor: greyColor,
},
top: {
backgroundColor : indigo,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
margin: 9,
},
headerText: {
fontSize: 18,
color: whiteColor,
textAlign: 'center',
fontWeight: 'bold',
},
img2: {
tintColor: whiteColor,
justifyContent: "center",
alignItems: "center",
},
button2:{
flexDirection: "row",
justifyContent: "center",
marginRight: 10,
borderRadius: 10,
alignItems: "center",
height: 30,
width: 70,
borderColor: darksalmonColor,
borderWidth: 1,
backgroundColor: darksalmonColor,
},
});

@ -1,33 +1,125 @@
import React from "react";
import React, {useContext, useEffect, useState} from "react";
import {
Appearance,
Button,
FlatList,
Image,
SafeAreaView,
ScrollView,
SectionListComponent,
StyleSheet, Switch, SwitchComponent,
Text, useColorScheme,
View
} from "react-native";
import usePersonalTheme, {
blackColor,
darksalmonColor,
greyColor,
indigo,
purpleColor,
whiteColor
} from "../Theme";
import {isEnabled} from "react-native/Libraries/Performance/Systrace";
import {DarkTheme, DefaultTheme, useTheme} from "@react-navigation/native";
import {storeTheme} from "../redux/store";
import {FlatList, Image, SafeAreaView, ScrollView, SectionListComponent, StyleSheet, Text, View} from "react-native";
import {indigo, purpleColor} from "../Theme";
export function SettingsScreen() {
const [isEnabled, setIsEnabled] = useState(false);
const toggleSwitch = () => {
setIsEnabled(previousState => {
const newIsEnabled = !previousState;
const newTheme = newIsEnabled ? DarkTheme : DefaultTheme;
console.log("newTheme", newTheme);
storeTheme(newTheme);
return newIsEnabled;
});
};
const styles = themeSettings();
return (
<View style={styles.font}>
<Text style={styles.text}>Settings Page</Text>
<Text style={styles.text}>In Work</Text>
</View>
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Réglages</Text>
<View style={styles.settingsContainer}>
<View style={styles.settingRow}>
<View style={styles.settingRow}>
<Image style={styles.img} source={require('../assets/darkmode_icon.png')}/>
<Text style={styles.settingText}>Dark Mode</Text>
</View>
<Switch style={styles.switch}
trackColor={{ false: whiteColor, true: darksalmonColor }}
thumbColor={isEnabled ? whiteColor :darksalmonColor}
onValueChange={toggleSwitch}
value={isEnabled}
/>
</View>
</View>
</SafeAreaView>
);
}
};
export function themeSettings() {
const theme = useTheme();
const colors = theme.colors;
console.log("themeSettings", colors)
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
header: {
padding: 16,
alignItems: 'center',
},
headerText: {
color: whiteColor,
fontSize: 18,
},
settingsContainer: {
backgroundColor: indigo,
margin: 16,
},
title: {
color: whiteColor,
fontSize: 20,
fontWeight: 'bold',
marginTop: 7,
marginLeft: 15,
marginBottom: 8,
},
settingRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
const styles = StyleSheet.create({
font: {
backgroundColor: indigo,
width: '100%',
height: '100%',
},
text: {
fontSize: 24,
color: 'darksalmon',
textAlign: 'center',
fontWeight: 'bold',
marginVertical: 20,
}
},
settingText: {
color: whiteColor,
marginLeft: 10,
},
img: {
marginLeft: 10,
width: 30,
height: 30,
tintColor: darksalmonColor,
},
switch: {
marginRight: 10,
}
});
return styles;
}
});

@ -0,0 +1,63 @@
import {describe,test ,expect, jest, it} from "@jest/globals";
import {CustomJoke} from "../../model/CustomJoke";
import {
CustomActionType,
getJokesCustoms,
setCustomsJoke,
setCustomsJokeById, setDeleteCustomJoke, setFavoriteJoke,
setPostJoke
} from "../../redux/actions/customAction";
describe('Action tests', () => {
it('setPostJoke creates correct action', () => {
const postJoke = new CustomJoke('test', 'Why did the chicken...', 'To get to the other side!', 'http://www.jokes.com/joke1', "1");
const expectedAction = {
type: CustomActionType.POST_CUSTOM_JOKE,
payload: postJoke,
};
expect(setPostJoke(postJoke)).toEqual(expectedAction);
});
it ('setCustomsJoke creates correct action', () => {
const customJokes = [new CustomJoke('test', 'Why did the chicken...', 'To get to the other side!', 'http://www.jokes.com/joke1', "1")];
const expectedAction = {
type: CustomActionType.FETCH_CUSTOMS_JOKE,
payload: customJokes,
};
expect(setCustomsJoke(customJokes)).toEqual(expectedAction);
}
);
it('setCustomsJokeById creates correct action', () => {
const completCustomJoke = new CustomJoke('test', 'Why did the chicken...', 'To get to the other side!', 'http://www.jokes.com/joke1', "1");
const expectedAction = {
type: CustomActionType.FETCH_CUSTOMS_JOKE_BY_ID,
payload: completCustomJoke,
};
expect(setCustomsJokeById(completCustomJoke)).toEqual(expectedAction);
}
);
it('setDeleteCustomJoke creates correct action', () => {
const deleteCustomJoke = new CustomJoke('test', 'Why did the chicken...', 'To get to the other side!', 'http://www.jokes.com/joke1', "1");
const expectedAction = {
type: CustomActionType.DELETE_CUSTOM_JOKE,
payload: deleteCustomJoke,
};
expect(setDeleteCustomJoke(deleteCustomJoke)).toEqual(expectedAction);
}
);
it('setFavoriteJoke creates correct action', () => {
const favoriteJokes = [new CustomJoke('test', 'Why did the chicken...', 'To get to the other side!', 'http://www.jokes.com/joke1', "1")];
const expectedAction = {
type: CustomActionType.FETCH_FAVORITE_JOKE,
payload: favoriteJokes,
};
expect(setFavoriteJoke(favoriteJokes)).toEqual(expectedAction);
}
);
});

@ -20,12 +20,22 @@ Chaque partie du projet est associée à une branche distincte dans ce dépôt.
- [x] Partie 1 - `part1`
- Création projet, création classes Typescript, création Stub pour ce modèle
- [ ] Partie 2 - `part2`
- *
- [ ] Partie 3 - `part3`
- *
- [ ] Partie 4 - `part4`
- *
- [x] Partie 2 - `part2`
- Creation page "Catalogue" + implementation FlatList de "SamplesJokes"
- [x] Partie 3 - `part3`
- Implementation navigation et création page "Accueil"
- [x] Partie 4 - `part4`
- Connection à l'<a href="https://iut-weather-api.azurewebsites.net/swagger-ui/#/Jokes">API Jokes</a>
- [x] Partie 5 - `part5`
- Implementation détail d'une "Joke" + StackNavigation
- [x] Partie 6 - `tp6`
- Création de "CustomsJokes"
- [x] Partie 7 - `tp7`
- Changement de theme et suppresion de "CustomsJokes"
- [x] Partie 8 - `pt8`
- Ajout de la fonctionnalité favorie
- [x] Partie 9 - `tp9`
- Réalisations des tests et déploiement sur sonar
## Installation et Configuration
1. Clonez le dépôt : `git clone https://codefirst.iut.uca.fr/git/tony.fages/Tp_ReactNative.git`

Loading…
Cancel
Save