Add JWT OAuth

pull/18/head
Anthony RICHARD 4 weeks ago
parent a3e567f291
commit 90b1e450e1

@ -1,13 +1,13 @@
import axios from 'axios';
import axios from "axios";
const apiClient = axios.create({
baseURL: 'https://codefirst.iut.uca.fr/containers/Optifit-optifit-ef-api/api/v1/',
timeout: 10000,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
// ajoute ici les headers supplémentaires, ex. Auth
},
baseURL:
"https://codefirst.iut.uca.fr/containers/Optifit-optifit-ef-api/api/v1/",
timeout: 10000,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
export default apiClient;

@ -1,30 +1,41 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { getItemAsync } from "expo-secure-store";
import apiClient from "../client";
import { AUTH } from "../endpoints";
const ACCESS_TOKEN_PATH = "access-token";
const REFRESH_TOKEN_PATH = "refresh-token";
export abstract class AbstractService {
protected async request<T>(callback: () => Promise<T>): Promise<T> {
try {
return await callback();
} catch (error: any) {
if (error.response?.status === 401) {
const success = await this.tryRefreshToken();
if (success) {
return await callback();
} else {
throw new Error("Session expired");
}
}
throw error;
}
protected IDP_URL =
"https://codefirst.iut.uca.fr/containers/Optifit-optifit-ef-api/api/v1";
protected URL =
"https://codefirst.iut.uca.fr/containers/Optifit-optifit-ef-api/api/v1";
protected CLIENT_ID = "mobile-app";
protected CLIENT_SECRET = "super-secret";
protected SCOPES =
"openid profile training.generate training.read offline_access";
protected ACCESS_TOKEN_PATH = "access-token";
protected REFRESH_TOKEN_PATH = "refresh-token";
protected async request(path: string, options: RequestInit = {}) {
const token = await getItemAsync(this.ACCESS_TOKEN_PATH);
const res = await fetch(`${this.URL}${path}`, {
...options,
headers: {
...(options.headers || {}),
Authorization: `Bearer ${token}`,
},
});
return token && res.status === 401
? await this.tryRefreshToken()
: await res.json();
}
private async tryRefreshToken(): Promise<boolean> {
try {
const refreshToken = await AsyncStorage.getItem(REFRESH_TOKEN_PATH);
const refreshToken = await AsyncStorage.getItem(this.REFRESH_TOKEN_PATH);
if (!refreshToken) return false;
@ -34,18 +45,15 @@ export abstract class AbstractService {
const { accessToken, refreshToken: newRefreshToken } = response.data;
// Save new tokens
await AsyncStorage.setItem(ACCESS_TOKEN_PATH, accessToken);
await AsyncStorage.setItem("refreshToken", newRefreshToken);
await AsyncStorage.setItem(this.ACCESS_TOKEN_PATH, accessToken);
await AsyncStorage.setItem(this.REFRESH_TOKEN_PATH, newRefreshToken);
// Update apiClient headers
apiClient.defaults.headers.common[
"Authorization"
] = `Bearer ${accessToken}`;
return true;
} catch (e) {
console.error("Refresh token failed", e);
return false;
}
}

@ -1,12 +0,0 @@
import apiClient from "@/api/client";
import { EXERCICES } from "@/api/endpoints";
import { AbstractService } from "../abstract.service";
export class ExerciceService extends AbstractService {
async getExercices() {
return this.request(async () => {
const response = await apiClient.get(EXERCICES.GETALL);
return response.data.data;
});
}
}

@ -0,0 +1,14 @@
import { EXERCICES } from "@/api/endpoints";
import { Workout } from "@/model/Workout";
import { AbstractService } from "../abstract.service";
import { IExerciceInterface } from "./exercice.service.interface";
export class ExerciceAPIService
extends AbstractService
implements IExerciceInterface
{
async getExercices(): Promise<Workout[]> {
const data = await this.request(EXERCICES.GETALL);
return data.data.map((item: any) => Workout.fromJson(item));
}
}

@ -0,0 +1,3 @@
export interface IExerciceInterface {
getExercices(): Promise<any>;
}

@ -0,0 +1,11 @@
import { AbstractService } from "../abstract.service";
import { IExerciceInterface } from "./exercice.service.interface";
export class ExerciceStubService
extends AbstractService
implements IExerciceInterface
{
async getExercices() {
return [];
}
}

@ -1,14 +1,31 @@
import apiClient from "@/api/client";
import { AUTH } from "@/api/endpoints";
import { User } from "@/model/User";
import { setItemAsync } from "expo-secure-store";
import { AbstractService as AbstractAPIService } from "../abstract.service";
import { IUserService } from "./user.service.interface";
export class UserAPIService extends AbstractAPIService implements IUserService {
async login(email: string, password: string): Promise<User> {
return this.request(async () => {
const response = await apiClient.get(AUTH.LOGIN);
return response.data.data;
const body = new URLSearchParams({
grant_type: "password",
client_id: email,
client_secret: this.CLIENT_SECRET,
email,
password,
scope: this.SCOPES,
});
const res = await fetch(`${this.IDP_URL}/connect/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
});
if (!res.ok) throw new Error(`Auth failed: ${res.status}`);
const json = await res.json();
await setItemAsync(this.ACCESS_TOKEN_PATH, json.access_token);
await setItemAsync(this.REFRESH_TOKEN_PATH, json.refresh_token);
return json;
}
}

@ -3,166 +3,154 @@ import { IUserService } from "./user.service.interface";
export class UserStubService implements IUserService {
private readonly users: User[] = [
new User(
"Alice",
28,
165,
58,
false,
"alice.png",
3,
"Perdre du poids",
[],
"YOGA",
"GOOD",
"BEGINNER",
"test@1.com",
"password1"
),
new User(
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
"test@2.com",
"password2"
),
new User(
"Charlie",
22,
172,
70,
true,
"charlie.png",
2,
"Se remettre en forme",
[],
"BIKING",
"GOOD",
"BEGINNER",
"test@3.com",
"password3"
),
new User(
"Diana",
31,
160,
55,
false,
"diana.png",
5,
"Préparer un marathon",
[],
"RUNNING",
"GOOD",
"VERY_SPORTY",
"test@4.com",
"password4"
),
new User(
"Ethan",
40,
180,
88,
true,
"ethan.png",
1,
"Maintenir sa forme",
["MIGRAINE"],
"WALKING",
"BAD",
"SPORTY",
"test@5.com",
"password5"
),
new User(
"Fiona",
26,
167,
62,
false,
"fiona.png",
3,
"Renforcer le dos",
["MIGRAINE"],
"CARDIO",
"BAD",
"BEGINNER",
"test@6.com",
"password6"
),
new User(
"George",
30,
185,
90,
true,
"george.png",
4,
"Perdre du gras",
[],
"BIKING",
"TERRIBLE",
"SPORTY",
"test@7.com",
"password7"
),
new User(
"Hanna",
24,
158,
54,
false,
"hanna.png",
2,
"Se tonifier",
[],
"RANDO",
"GOOD",
"BEGINNER",
"test@8.com",
"password8"
),
new User(
"Ivan",
50,
175,
95,
true,
"ivan.png",
1,
"Rééducation",
["ARTHROSE"],
"WALKING",
"BAD",
"BEGINNER",
"test@9.com",
"password9"
),
new User(
"Julia",
29,
170,
60,
false,
"julia.png",
3,
"Rester active",
[],
"ELSE",
"GOOD",
"SPORTY",
"test@10.com",
"password10"
),
new User({
name: "Alice",
age: 28,
height: 165,
weight: 58,
sexe: false,
logo: "alice.png",
nbSessionPerWeek: 3,
goal: "Perdre du poids",
healthProblems: [],
sport: "YOGA",
sleepLevel: "GOOD",
sportLevel: "BEGINNER",
email: "test@1.com",
password: "password1",
}),
new User({
email: "test@2.com",
password: "password2",
}),
new User({
name: "Charlie",
age: 22,
height: 172,
weight: 70,
sexe: true,
logo: "charlie.png",
nbSessionPerWeek: 2,
goal: "Se remettre en forme",
healthProblems: [],
sport: "BIKING",
sleepLevel: "GOOD",
sportLevel: "BEGINNER",
email: "test@3.com",
password: "password3",
}),
new User({
name: "Diana",
age: 31,
height: 160,
weight: 55,
sexe: false,
logo: "diana.png",
nbSessionPerWeek: 5,
goal: "Préparer un marathon",
healthProblems: [],
sport: "RUNNING",
sleepLevel: "GOOD",
sportLevel: "VERY_SPORTY",
email: "test@4.com",
password: "password4",
}),
new User({
name: "Ethan",
age: 40,
height: 180,
weight: 88,
sexe: true,
logo: "ethan.png",
nbSessionPerWeek: 1,
goal: "Maintenir sa forme",
healthProblems: ["MIGRAINE"],
sport: "WALKING",
sleepLevel: "BAD",
sportLevel: "SPORTY",
email: "test@5.com",
password: "password5",
}),
new User({
name: "Fiona",
age: 26,
height: 167,
weight: 62,
sexe: false,
logo: "fiona.png",
nbSessionPerWeek: 3,
goal: "Renforcer le dos",
healthProblems: ["MIGRAINE"],
sport: "CARDIO",
sleepLevel: "BAD",
sportLevel: "BEGINNER",
email: "test@6.com",
password: "password6",
}),
new User({
name: "George",
age: 30,
height: 185,
weight: 90,
sexe: true,
logo: "george.png",
nbSessionPerWeek: 4,
goal: "Perdre du gras",
healthProblems: [],
sport: "BIKING",
sleepLevel: "TERRIBLE",
sportLevel: "SPORTY",
email: "test@7.com",
password: "password7",
}),
new User({
name: "Hanna",
age: 24,
height: 158,
weight: 54,
sexe: false,
logo: "hanna.png",
nbSessionPerWeek: 2,
goal: "Se tonifier",
healthProblems: [],
sport: "RANDO",
sleepLevel: "GOOD",
sportLevel: "BEGINNER",
email: "test@8.com",
password: "password8",
}),
new User({
name: "Ivan",
age: 50,
height: 175,
weight: 95,
sexe: true,
logo: "ivan.png",
nbSessionPerWeek: 1,
goal: "Rééducation",
healthProblems: ["ARTHROSE"],
sport: "WALKING",
sleepLevel: "BAD",
sportLevel: "BEGINNER",
email: "test@9.com",
password: "password9",
}),
new User({
name: "Julia",
age: 29,
height: 170,
weight: 60,
sexe: false,
logo: "julia.png",
nbSessionPerWeek: 3,
goal: "Rester active",
healthProblems: [],
sport: "ELSE",
sleepLevel: "GOOD",
sportLevel: "SPORTY",
email: "test@10.com",
password: "password10",
}),
];
async login(email: string, password: string): Promise<User> {
@ -171,7 +159,7 @@ export class UserStubService implements IUserService {
);
if (!user) {
throw new Error("No user.");
throw new Error("User not found.");
}
return user;

@ -1,13 +1,13 @@
import BackButton from "@/components/BackButton";
import { EMPTY_FIELD, INVALID_EMAIL } from "@/components/Errors";
import FormError from "@/components/form/FormError";
import TextInput from "@/components/form/FormInput";
import CodeSent from "@/components/modals/CodeSent";
import Button from "@/components/ui/Button";
import Screen from "@/components/ui/Screen";
import Text from "@/components/ui/Text";
import TextInput from "@/components/form/FormInput";
import BackButton from "@/components/BackButton";
import { View } from "react-native";
import React from "react";
import CodeSent from "@/components/modals/CodeSent";
import FormError from "@/components/form/FormError";
import { EMPTY_FIELD, INVALID_EMAIL } from "@/components/Errors";
import { View } from "react-native";
import { isEmail } from "validator";
export default function ResetPasswordWithEmail() {

@ -1,16 +1,16 @@
import {SafeAreaView, View, Text} from "react-native";
import React from "react";
import InternalError from "@/components/error/InternalErrorProblem";
import React from "react";
import { SafeAreaView, View } from "react-native";
export default function AddScreen() {
return (
<SafeAreaView>
<View>
<InternalError/>
</View>
</SafeAreaView>
);
return (
<SafeAreaView>
<View>
<InternalError />
</View>
</SafeAreaView>
);
}
//<Text className="m-7 font-extrabold">Welcome to Add Screen </Text>
// <Text>We will do it soon</Text>
// <Text>We will do it soon</Text>

@ -1,11 +1,13 @@
import React from "react";
import { Stack } from "expo-router";
import React from "react";
export default function RootoLayout() {
return (
<Stack screenOptions={{
headerShown: false,
}}>
<Stack
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="index" />
</Stack>
);

@ -1,11 +1,11 @@
import { ExerciceService } from "@/api/services/exercice/ExercicesServices";
import { ExerciceAPIService } from "@/api/services/exercice/exercice.service.api";
import WorkoutCardComponent from "@/components/WorkoutCardComponent";
import { Workout } from "@/model/Workout";
import { useRouter } from "expo-router";
import React, { useEffect, useState } from "react";
import { FlatList, Text, TouchableOpacity, View } from "react-native";
const service = new ExerciceService();
const service = new ExerciceAPIService();
export default function ExercicesScreen() {
const [exercices, setExercices] = useState<Workout[]>([]);

@ -1,10 +1,10 @@
import { ExerciceService } from "@/api/services/exercice/ExercicesServices";
import { ExerciceAPIService } from "@/api/services/exercice/exercice.service.api";
import WorkoutPresentationComponent from "@/components/WorkoutPresentationComponent";
import { useRouter } from "expo-router";
import React, { useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
const service = new ExerciceService();
const service = new ExerciceAPIService();
export default function WorkoutScreen() {
const router = useRouter();

@ -1,19 +1,17 @@
import { Stack } from "expo-router";
import React from "react";
import {Stack} from "expo-router";
export default function RootoLayout() {
return (
<Stack screenOptions={{
headerShown: false,
}}
initialRouteName={"ExercicesScreen"}
>
<Stack.Screen name="ExercicesScreen" />
return (
<Stack
screenOptions={{
headerShown: false,
}}
initialRouteName={"ExercicesScreen"}
>
<Stack.Screen name="ExercicesScreen" />
<Stack.Screen name="WorkoutScreen"/>
</Stack>
);
}
<Stack.Screen name="WorkoutScreen" />
</Stack>
);
}

@ -1,14 +1,13 @@
import {SafeAreaView, Text, View} from "react-native";
import React from "react";
import Blocked from "@/components/error/BlockedProblem";
import React from "react";
import { SafeAreaView, View } from "react-native";
export default function HelpsScreen() {
return (
<SafeAreaView>
<View>
<Blocked/>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView>
<View>
<Blocked />
</View>
</SafeAreaView>
);
}

@ -1,15 +1,14 @@
import { Stack } from "expo-router";
import React from "react";
import {Stack} from "expo-router";
import HelpsScreen from "@/app/(tabs)/(help)/HelpsScreen";
export default function RootoLayout() {
return (
<Stack screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="HelpsScreen" />
</Stack>
);
}
return (
<Stack
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="HelpsScreen" />
</Stack>
);
}

@ -1,4 +1,4 @@
import { ExerciceService } from "@/api/services/exercice/ExercicesServices";
import { ExerciceAPIService } from "@/api/services/exercice/exercice.service.api";
import ActivitiesComponent from "@/components/ActivitiesComponent";
import CalendarComponent from "@/components/CalendarComponent";
import WelcomeComponent from "@/components/WelcomeComponent";
@ -8,7 +8,7 @@ import { Workout } from "@/model/Workout";
import React, { useEffect, useState } from "react";
import { ScrollView, Text, View } from "react-native";
const service = new ExerciceService();
const service = new ExerciceAPIService();
export default function HomeScreen() {
const [exercices, setExercices] = useState<Workout[]>([]);

@ -3,9 +3,11 @@ import React from "react";
export default function RootoLayout() {
return (
<Stack screenOptions={{
headerShown: false,
}}>
<Stack
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="HomeScreen" />
</Stack>
);

@ -1,10 +1,6 @@
import {SafeAreaView, Text, View} from "react-native";
import React from "react";
import HomeScreen from "@/app/(tabs)/(home)/HomeScreen";
import React from "react";
export default function App() {
return (
<HomeScreen/>
);
}
return <HomeScreen />;
}

@ -1,8 +1,8 @@
import { SafeAreaView, Text, View } from "react-native";
import React from "react";
import Button from "@/components/ui/Button";
import { useSession } from "@/ctx";
import { router } from "expo-router";
import React from "react";
import { SafeAreaView, Text, View } from "react-native";
export default function ProfileScreen() {
const { signOut } = useSession();

@ -3,10 +3,12 @@ import React from "react";
export default function RootoLayout() {
return (
<Stack screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="ProfileScreen" />
</Stack>
<Stack
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="ProfileScreen" />
</Stack>
);
}

@ -1,37 +1,50 @@
import Text from "@/components/ui/Text";
import React, {forwardRef} from "react";
import {Image, View, ViewProps} from "react-native";
import BackButton from "@/components/BackButton";
import Text from "@/components/ui/Text";
import React, { forwardRef } from "react";
import { Image, View, ViewProps } from "react-native";
import {Entypo} from "@expo/vector-icons";
import { Entypo } from "@expo/vector-icons";
export interface ProblemProps extends ViewProps{
picture: string;
problem: string;
description: string;
information: string;
isVisible?: boolean;
export interface ProblemProps extends ViewProps {
picture: string;
problem: string;
description: string;
information: string;
isVisible?: boolean;
}
export default forwardRef<any, ProblemProps> (({className, ...Props}, ref) => {
return (
<View className={"gap-4 justify-between h-3/4" + " " + className} {...Props} ref={ref}>
<View className="flex-row justify-between items-center p-4">
<BackButton/>
</View>
export default forwardRef<any, ProblemProps>(({ className, ...Props }, ref) => {
return (
<View
className={"gap-4 justify-between h-3/4" + " " + className}
{...Props}
ref={ref}
>
<View className="flex-row justify-between items-center p-4">
<BackButton />
</View>
<View className="flex-row justify-center">
<Image className="aspect-square w-3/5 h-3/5" source={Props.picture}/>
</View>
<View className="flex-row justify-center">
<Image className="aspect-square w-3/5 h-3/5" source={Props.picture} />
</View>
<Text position="center" weight="bold" size="3xl"> {Props.problem} </Text>
<Text size="lg" position="center" className="text-gray-400"> {Props.description} </Text>
<View className="flex-row justify-center">
<View className="flex-row items-center border-2 rounded-2xl bg-red-300 border-red-600 p-4">
<Entypo name="warning" size={30} color="red"/>
<Text size="lg" position="center"> {Props.information} </Text>
</View>
</View>
<Text position="center" weight="bold" size="3xl">
{" "}
{Props.problem}{" "}
</Text>
<Text size="lg" position="center" className="text-gray-400">
{" "}
{Props.description}{" "}
</Text>
<View className="flex-row justify-center">
<View className="flex-row items-center border-2 rounded-2xl bg-red-300 border-red-600 p-4">
<Entypo name="warning" size={30} color="red" />
<Text size="lg" position="center">
{" "}
{Props.information}{" "}
</Text>
</View>
);
});
</View>
</View>
);
});

@ -1,113 +1,114 @@
import {Text, TouchableOpacity, View} from "react-native";
import {CurveType, LineChart} from "react-native-gifted-charts";
import React, {useRef, useState} from "react";
import React, { useRef, useState } from "react";
import { Text, TouchableOpacity, View } from "react-native";
import { CurveType, LineChart } from "react-native-gifted-charts";
export default function ActivitiesComponent() {
const ref = useRef(null)
const [dateSelected, setDateSelected] = useState('1d');
const [lineData, setLineData] = useState([
{value: 4},
{value: 14},
{value: 8},
{value: 38},
{value: 36},
{value: 28},
]);
const ref = useRef(null);
const [dateSelected, setDateSelected] = useState("1d");
const [lineData, setLineData] = useState([
{ value: 4 },
{ value: 14 },
{ value: 8 },
{ value: 38 },
{ value: 36 },
{ value: 28 },
]);
const months = ['1d','1w','1m','1y','All']
const changeMonthSelected = (ind: number) => {
const selectedMonth = months[ind];
setDateSelected(selectedMonth);
const months = ["1d", "1w", "1m", "1y", "All"];
const changeMonthSelected = (ind: number) => {
const selectedMonth = months[ind];
setDateSelected(selectedMonth);
// Update lineData based on the selected month
let newData: React.SetStateAction<{ value: number; }[]>;
switch (selectedMonth) {
case '1d':
newData = [
{value: 4},
{value: 14},
{value: 8},
{value: 38},
{value: 36},
{value: 28},
];
break;
case '1w':
newData = [
{value: 8},
{value: 14},
{value: 8},
{value: 38},
{value: 14},
{value: 28},
{value: 4},
];
break;
case '1m':
newData = [
{value: 10},
{value: 20},
{value: 30},
{value: 40},
{value: 50},
{value: 60},
];
break;
case '1y':
newData = [
{value: 15},
{value: 25},
{value: 35},
{value: 45},
{value: 55},
{value: 65},
];
break;
case 'All':
newData = [
{value: 5},
{value: 15},
{value: 25},
{value: 35},
{value: 45},
{value: 55},
];
break;
default:
newData = [];
}
setLineData(newData);
};
// Update lineData based on the selected month
let newData: React.SetStateAction<{ value: number }[]>;
switch (selectedMonth) {
case "1d":
newData = [
{ value: 4 },
{ value: 14 },
{ value: 8 },
{ value: 38 },
{ value: 36 },
{ value: 28 },
];
break;
case "1w":
newData = [
{ value: 8 },
{ value: 14 },
{ value: 8 },
{ value: 38 },
{ value: 14 },
{ value: 28 },
{ value: 4 },
];
break;
case "1m":
newData = [
{ value: 10 },
{ value: 20 },
{ value: 30 },
{ value: 40 },
{ value: 50 },
{ value: 60 },
];
break;
case "1y":
newData = [
{ value: 15 },
{ value: 25 },
{ value: 35 },
{ value: 45 },
{ value: 55 },
{ value: 65 },
];
break;
case "All":
newData = [
{ value: 5 },
{ value: 15 },
{ value: 25 },
{ value: 35 },
{ value: 45 },
{ value: 55 },
];
break;
default:
newData = [];
}
setLineData(newData);
};
return (
<View className="bg-gray-200 rounded-2xl p-1 h-full">
<View className=" m-2 flex-row justify-center rounded-2xl bg-white">
{months.map((item, index) => {
return (
<TouchableOpacity
key={index}
className={`w-16 h-10 flex items-center justify-center rounded-xl ${
dateSelected === item
? "bg-orange-500 border-2 border-orange-300"
: "bg-transparent "
}`}
onPress={() => changeMonthSelected(index)}>
<Text className="font-extrabold">{months[index]}</Text>
</TouchableOpacity>
);
})}
</View>
<LineChart
scrollRef={ref}
data={lineData}
areaChart
curved
initialSpacing={0}
rotateLabel
isAnimated={true}
startFillColor="orange"
curveType={CurveType.QUADRATIC}
/>
</View>
);
}
return (
<View className="bg-gray-200 rounded-2xl p-1 h-full">
<View className=" m-2 flex-row justify-center rounded-2xl bg-white">
{months.map((item, index) => {
return (
<TouchableOpacity
key={index}
className={`w-16 h-10 flex items-center justify-center rounded-xl ${
dateSelected === item
? "bg-orange-500 border-2 border-orange-300"
: "bg-transparent "
}`}
onPress={() => changeMonthSelected(index)}
>
<Text className="font-extrabold">{months[index]}</Text>
</TouchableOpacity>
);
})}
</View>
<LineChart
scrollRef={ref}
data={lineData}
areaChart
curved
initialSpacing={0}
rotateLabel
isAnimated={true}
startFillColor="orange"
curveType={CurveType.QUADRATIC}
/>
</View>
);
}

@ -1,51 +1,51 @@
import {FlatList, TouchableOpacity,Text, View} from "react-native";
import {useState} from "react";
import dayjs from "dayjs";
import { useState } from "react";
import { FlatList, Text, TouchableOpacity, View } from "react-native";
export default function CalendarComponent() {
const [selectedDay] = useState(dayjs().date());
const [selectedDay] = useState(dayjs().date());
const days = Array.from({ length: 7 }, (_, index) => {
const day = dayjs().add(index, "day");
return {
id: day.date(),
label: day.format("ddd"),
};
});
const days = Array.from({ length: 7 }, (_, index) => {
const day = dayjs().add(index, "day");
return {
id: day.date(),
label: day.format("ddd"),
};
});
return (
<View className="bg-transparent">
<FlatList
horizontal
data={days}
keyExtractor={(item) => item.id.toString()}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ gap: 12 }} // Espacement entre les items
renderItem={({ item }) => (
<TouchableOpacity
className={`w-16 h-20 flex items-center justify-center rounded-xl ${
selectedDay === item.id
? "bg-orange-500 border-2 border-orange-300"
: "bg-black"
}`}
>
<Text
className={`text-sm ${
selectedDay === item.id ? "text-white" : "text-gray-400"
}`}
>
{item.label}
</Text>
<Text
className={`text-lg font-bold ${
selectedDay === item.id ? "text-white" : "text-gray-200"
}`}
>
{item.id}
</Text>
</TouchableOpacity>
)}
/>
</View>
);
}
return (
<View className="bg-transparent">
<FlatList
horizontal
data={days}
keyExtractor={(item) => item.id.toString()}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ gap: 12 }} // Espacement entre les items
renderItem={({ item }) => (
<TouchableOpacity
className={`w-16 h-20 flex items-center justify-center rounded-xl ${
selectedDay === item.id
? "bg-orange-500 border-2 border-orange-300"
: "bg-black"
}`}
>
<Text
className={`text-sm ${
selectedDay === item.id ? "text-white" : "text-gray-400"
}`}
>
{item.label}
</Text>
<Text
className={`text-lg font-bold ${
selectedDay === item.id ? "text-white" : "text-gray-200"
}`}
>
{item.id}
</Text>
</TouchableOpacity>
)}
/>
</View>
);
}

@ -1,6 +1,3 @@
import React, { forwardRef } from "react";
import { View, TouchableOpacity, ViewProps } from "react-native";
import Text from "./ui/Text";
import {
AntDesign,
Entypo,
@ -8,6 +5,8 @@ import {
Ionicons,
MaterialCommunityIcons,
} from "@expo/vector-icons";
import React, { forwardRef } from "react";
import { TouchableOpacity, View, ViewProps } from "react-native";
import {
AntDesignIconNames,
CommunityIconNames,
@ -16,6 +15,7 @@ import {
FontAwesomeIconNames,
IonIconNames,
} from "./Icons";
import Text from "./ui/Text";
export type CheckBoxDirection = "row" | "col";

@ -5,6 +5,8 @@ export const NOT_MATCHING_PASSWORD = "Les mots de passe sont différents";
export const NOT_FOUND = "Ressource introuvable :<";
export const NO_INTERNET = "Pas de connexion à internet";
export const INTERNAL_ERROR = "Erreur interne, veuillez nous pardonner";
export const MAINTENANCE = "Le serveur est en maintenance, veuillez réessayer plus tard";
export const NOT_AUTHORIZED = "Vous n'êtes pas autorisé à accéder à cette ressource";
export const FEATURE_LOCKED = "Cette fonctionnalité est verrouillée";
export const MAINTENANCE =
"Le serveur est en maintenance, veuillez réessayer plus tard";
export const NOT_AUTHORIZED =
"Vous n'êtes pas autorisé à accéder à cette ressource";
export const FEATURE_LOCKED = "Cette fonctionnalité est verrouillée";

@ -49,9 +49,7 @@ export default function LinearProgressBar({ duration = 10 }) {
<TouchableOpacity
onPress={startAnimation}
disabled={isRunning}
className={`px-4 py-2 rounded-full ${
isRunning ? "bg-orange-400" : "bg-orange-400"
}`}
className={"px-4 py-2 rounded-full bg-orange-400"}
>
<Text className="text-white font-bold">
{isRunning ? "En cours..." : "Play"}

@ -7,23 +7,15 @@ import { ImageBackground, TouchableOpacity, View } from "react-native";
interface WorkoutCardComponentProps {
exercise?: Workout;
background?: String;
background?: string;
height?: number;
}
export default function WorkoutCardComponent({
exercise,
height,
background,
}: WorkoutCardComponentProps) {
const style = () => {
return `h-full rounded-2xl overflow-hidden ${background ?? "bg-black"}`;
};
const styleImage = () => {
return `w-full h-full `;
};
}: Readonly<WorkoutCardComponentProps>) {
const router = useRouter();
return (
<View className="h-full rounded-2xl overflow-hidden bg-black">
<ImageBackground

@ -9,12 +9,12 @@ import { ImageBackground, Text, TouchableOpacity, View } from "react-native";
type WorkoutPresentationComponentProps = {
workout: Workout;
dataExercise: Workout[];
router: Router; // Typage précis recommandé selon ta navigation
router: Router;
};
export default function WorkoutPresentationComponent({
workout,
}: WorkoutPresentationComponentProps) {
}: Readonly<WorkoutPresentationComponentProps>) {
const router = useRouter();
return (
<ImageBackground
@ -38,10 +38,8 @@ export default function WorkoutPresentationComponent({
</TouchableOpacity>
</View>
{/* Permet de pousser le reste du contenu vers le bas */}
<View className="flex-grow" />
{/* Texte en bas */}
<View className="items-center mb-10">
<Text className="text-white bg-transparent border-2 border-white px-3 py-1 rounded-full text-2xl font-bold">
{workout.nbSeries} x {workout.nbRepetitions}
@ -52,7 +50,6 @@ export default function WorkoutPresentationComponent({
</Text>
</View>
{/* Barre de progression */}
<View className="mb-5">
<LinearProgressBar duration={workout.duration} />
</View>

@ -1,15 +1,16 @@
import React from "react";
import {FEATURE_LOCKED} from "@/components/Errors";
import Error from "@/app/(utility)/Error";
//@ts-ignore
import blockedPict from "@/assets/images/Blocked.png";
import { FEATURE_LOCKED } from "@/components/Errors";
import React from "react";
export default function Blocked() {
return (
<Error
picture={blockedPict}
problem="Fonctionnalité bloquée"
description={FEATURE_LOCKED}
information="Devenez PREMIUM pour débloquer"
/>
);
}
return (
<Error
picture={blockedPict}
problem="Fonctionnalité bloquée"
description={FEATURE_LOCKED}
information="Devenez PREMIUM pour débloquer"
/>
);
}

@ -1,15 +1,16 @@
import React from "react";
import {INTERNAL_ERROR} from "@/components/Errors";
import Error from "@/app/(utility)/Error";
//@ts-ignore
import internalErrorPict from "@/assets/images/InternalError.png";
import { INTERNAL_ERROR } from "@/components/Errors";
import React from "react";
export default function InternalError() {
return (
<Error
picture={internalErrorPict}
problem="Problème interne"
description={INTERNAL_ERROR}
information="Contactez le support"
/>
);
}
return (
<Error
picture={internalErrorPict}
problem="Problème interne"
description={INTERNAL_ERROR}
information="Contactez le support"
/>
);
}

@ -1,15 +1,16 @@
import React from "react";
import {MAINTENANCE} from "@/components/Errors";
import Error from "@/app/(utility)/Error";
//@ts-ignore
import maintenancePict from "@/assets/images/Maintenance.png";
import { MAINTENANCE } from "@/components/Errors";
import React from "react";
export default function Maintenance() {
return (
<Error
picture={maintenancePict}
problem="Maintenance"
description={MAINTENANCE}
information="Revenez plus tard"
/>
);
}
return (
<Error
picture={maintenancePict}
problem="Maintenance"
description={MAINTENANCE}
information="Revenez plus tard"
/>
);
}

@ -1,15 +1,16 @@
import React from "react";
import {NO_INTERNET} from "@/components/Errors";
import Error from "@/app/(utility)/Error";
//@ts-ignore
import noInternetPict from "@/assets/images/NoInternet.png";
import { NO_INTERNET } from "@/components/Errors";
import React from "react";
export default function NoInternet() {
return (
<Error
picture={noInternetPict}
problem="Pas d'internet"
description={NO_INTERNET}
information="Réessayez plus tard"
/>
);
}
return (
<Error
picture={noInternetPict}
problem="Pas d'internet"
description={NO_INTERNET}
information="Réessayez plus tard"
/>
);
}

@ -1,15 +1,16 @@
import React from "react";
import {NOT_AUTHORIZED} from "@/components/Errors";
import Error from "@/app/(utility)/Error";
//@ts-ignore
import notAllowedPict from "@/assets/images/NotAllowed.png";
import { NOT_AUTHORIZED } from "@/components/Errors";
import React from "react";
export default function NotAllowedProblem() {
return (
<Error
picture={notAllowedPict}
problem="Pas autorisé"
description={NOT_AUTHORIZED}
information="Connectez vous avec plus de privilèges"
/>
);
}
return (
<Error
picture={notAllowedPict}
problem="Pas autorisé"
description={NOT_AUTHORIZED}
information="Connectez vous avec plus de privilèges"
/>
);
}

@ -1,15 +1,16 @@
import React from "react";
import {NOT_FOUND} from "@/components/Errors";
import Error from "@/app/(utility)/Error";
//@ts-ignore
import notFoundPict from "@/assets/images/NotFound.png";
import { NOT_FOUND } from "@/components/Errors";
import React from "react";
export default function NotFound() {
return (
<Error
picture={notFoundPict}
problem="Introuvable"
description={NOT_FOUND}
information="Status Code : 404"
/>
);
}
return (
<Error
picture={notFoundPict}
problem="Introuvable"
description={NOT_FOUND}
information="Status Code : 404"
/>
);
}

@ -35,13 +35,13 @@ export default React.forwardRef<any, ViewProps>(
if (isEmail(email)) {
if (password != "") {
validateForm();
const user = userService.login(email, password);
const user = userService.login(email, password).catch((e) => {
invalidateForm("Email ou mot de passe incorrect");
});
user.then((u) => {
if (u) {
signIn(u);
router.replace("/HomeScreen");
} else {
invalidateForm("Email ou mot de passe incorrect");
}
});
} else {

@ -1,9 +1,9 @@
import { AntDesign } from "@expo/vector-icons";
import React from "react";
import { TouchableOpacity, TouchableOpacityProps, View } from "react-native";
import CustomText from "./Text";
import { AntDesign } from "@expo/vector-icons";
import { AntDesignIconNames } from "../Icons";
import { Size } from "../Constants";
import { AntDesignIconNames } from "../Icons";
import CustomText from "./Text";
export type ButtonStyle = "default" | "outline" | "secondary";

@ -1,6 +1,6 @@
import React from "react";
import { View, ViewProps } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import React from "react";
export default React.forwardRef<any, ViewProps>(
({ children, ...props }, ref): React.ReactElement => {

@ -1,7 +1,7 @@
import React from "react";
import SegmentedControl, {
SegmentedControlProps,
} from "@react-native-segmented-control/segmented-control";
import React from "react";
export default React.forwardRef<any, SegmentedControlProps>(
(props, ref): React.ReactElement => {

@ -1,5 +1,5 @@
import React from "react";
import Slider, { SliderProps } from "@react-native-community/slider";
import React from "react";
export default React.forwardRef<any, SliderProps>(
({ ...props }, ref): React.ReactElement => {

@ -27,7 +27,7 @@ export function SessionProvider(props: Readonly<React.PropsWithChildren>) {
<AuthContext.Provider
value={{
signIn: (user: User) => {
setSessionStr(JSON.stringify(user));
setSessionStr(user.toJSON());
},
signOut: () => {
setSessionStr(null);

@ -6,37 +6,52 @@ import {
} from "./enums/Enums";
export class User {
private _name: string | undefined;
private _age: number | undefined;
private _height: number | undefined;
private _weight: number | undefined;
private _sexe: boolean | undefined; // true = Male, false = Female
private _logo: string | undefined;
private _nbSessionPerWeek: number | undefined;
private _goal: string | undefined;
private _healthProblems: EHealthProblem[] | undefined;
private _sport: ESport | undefined;
private _sleepLevel: ESleepLevel | undefined;
private _sportLevel: ESportLevel | undefined;
private _email: string;
private _password: string;
constructor(
name: string | undefined,
age: number | undefined,
height: number | undefined,
weight: number | undefined,
sexe: boolean | undefined,
logo: string | undefined,
nbSessionPerWeek: number | undefined,
goal: string | undefined,
healthProblems: EHealthProblem[] | undefined,
sport: ESport | undefined,
sleepLevel: ESleepLevel | undefined,
sportLevel: ESportLevel | undefined,
email: string,
password: string
) {
private _name?: string;
private _age?: number;
private _height?: number;
private _weight?: number;
private _sexe?: boolean; // true = Male, false = Female
private _logo?: string;
private _nbSessionPerWeek?: number;
private _goal?: string;
private _healthProblems?: EHealthProblem[];
private _sport?: ESport;
private _sleepLevel?: ESleepLevel;
private _sportLevel?: ESportLevel;
private _email?: string;
private _password?: string;
constructor({
name,
age,
height,
weight,
sexe,
logo,
nbSessionPerWeek,
goal,
healthProblems,
sport,
sleepLevel,
sportLevel,
email,
password,
}: {
name?: string;
age?: number;
height?: number;
weight?: number;
sexe?: boolean;
logo?: string;
nbSessionPerWeek?: number;
goal?: string;
healthProblems?: EHealthProblem[];
sport?: ESport;
sleepLevel?: ESleepLevel;
sportLevel?: ESportLevel;
email?: string;
password?: string;
} = {}) {
this._name = name;
this._age = age;
this._height = height;
@ -82,7 +97,7 @@ export class User {
return this._nbSessionPerWeek;
}
get goals(): string | undefined {
get goal(): string | undefined {
return this._goal;
}
@ -90,7 +105,7 @@ export class User {
return this._healthProblems;
}
get sports(): ESport | undefined {
get sport(): ESport | undefined {
return this._sport;
}
@ -102,11 +117,11 @@ export class User {
return this._sportLevel;
}
get email(): string {
get email(): string | undefined {
return this._email;
}
get password(): string {
get password(): string | undefined {
return this._password;
}
@ -139,7 +154,7 @@ export class User {
this._nbSessionPerWeek = value;
}
set goals(value: string | undefined) {
set goal(value: string | undefined) {
this._goal = value;
}
@ -147,7 +162,7 @@ export class User {
this._healthProblems = value;
}
set sports(value: ESport | undefined) {
set sport(value: ESport | undefined) {
this._sport = value;
}
@ -184,21 +199,40 @@ export class User {
}
static fromJSON(json: any): User {
return new User(
json._name,
json._age,
json._height,
json._weight,
json._sexe,
json._logo,
json._nbSessionPerWeek,
json._goal,
json._healthProblems,
json._sport,
json._sleepLevel,
json._sportLevel,
json._email,
json._password
);
return new User({
name: json.name,
age: json.age,
height: json.height,
weight: json.weight,
sexe: json.sexe,
logo: json.logo,
nbSessionPerWeek: json.nbSessionPerWeek,
goal: json.goal,
healthProblems: json.healthProblems,
sport: json.sport,
sleepLevel: json.sleepLevel,
sportLevel: json.sportLevel,
email: json.email,
password: json.password,
});
}
toJSON(): any {
return JSON.stringify({
name: this.name,
age: this.age,
height: this.height,
weight: this.weight,
sexe: this.sexe,
logo: this.logo,
nbSessionPerWeek: this.nbSessionPerWeek,
goal: this.goal,
healthProblems: this.healthProblems,
sport: this.sport,
sleepLevel: this.sleepLevel,
sportLevel: this.sportLevel,
email: this.email,
password: this.password,
});
}
}

@ -1,10 +1,118 @@
export interface Workout {
id: string;
name: string;
description: string;
duration: number;
image: string;
video: string;
nbSeries: number;
nbRepetitions: number;
export class Workout {
private _id?: string;
private _name?: string;
private _description?: string;
private _duration?: number;
private _image?: string;
private _video?: string;
private _nbSeries?: number;
private _nbRepetitions?: number;
constructor({
id,
name,
description,
duration,
image,
video,
nbSeries,
nbRepetitions,
}: {
id?: string;
name?: string;
description?: string;
duration?: number;
image?: string;
video?: string;
nbSeries?: number;
nbRepetitions?: number;
} = {}) {
this._id = id;
this._name = name;
this._description = description;
this._duration = duration;
this._image = image;
this._video = video;
this._nbSeries = nbSeries;
this._nbRepetitions = nbRepetitions;
}
// Getters
get id(): string | undefined {
return this._id;
}
get name(): string | undefined {
return this._name;
}
get description(): string | undefined {
return this._description;
}
get duration(): number | undefined {
return this._duration;
}
get image(): string | undefined {
return this._image;
}
get video(): string | undefined {
return this._video;
}
get nbSeries(): number | undefined {
return this._nbSeries;
}
get nbRepetitions(): number | undefined {
return this._nbRepetitions;
}
// Setters
set id(value: string | undefined) {
this._id = value;
}
set name(value: string | undefined) {
this._name = value;
}
set description(value: string | undefined) {
this._description = value;
}
set duration(value: number | undefined) {
this._duration = value;
}
set image(value: string | undefined) {
this._image = value;
}
set video(value: string | undefined) {
this._video = value;
}
set nbSeries(value: number | undefined) {
this._nbSeries = value;
}
set nbRepetitions(value: number | undefined) {
this._nbRepetitions = value;
}
static fromJson(json: any): Workout {
return new Workout({
id: json.id,
name: json.name,
description: json.description,
duration: json.duration,
image: json.image,
video: json.video,
nbSeries: json.nbSeries,
nbRepetitions: json.nbRepetitions,
});
}
}

2
package-lock.json generated

@ -28,7 +28,7 @@
"expo-linear-gradient": "^14.0.2",
"expo-linking": "^7.0.5",
"expo-router": "~5.0.7",
"expo-secure-store": "^14.0.1",
"expo-secure-store": "^14.2.3",
"expo-splash-screen": "~0.30.8",
"expo-status-bar": "^2.0.1",
"expo-symbols": "~0.4.4",

@ -35,7 +35,7 @@
"expo-linear-gradient": "^14.0.2",
"expo-linking": "^7.0.5",
"expo-router": "~5.0.7",
"expo-secure-store": "^14.0.1",
"expo-secure-store": "^14.2.3",
"expo-splash-screen": "~0.30.8",
"expo-status-bar": "^2.0.1",
"expo-symbols": "~0.4.4",

Loading…
Cancel
Save