Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion front/assets/translations/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@
},
"common": {
"cancel": "Cancel",
"delete": "Delete"
"delete": "Delete",
"confirm": "Confirm"
},
"jobOffer": {
"info": {
"jobOfferList": "Job offer list",
"jobOffer": "Job offer"
},
"apply": {
"apply": "Apply",
"applyTitle": "Apply confirmation",
"applyConfirm": "Do you want to apply to this job?"
}
},
"profile": {
Expand Down
9 changes: 8 additions & 1 deletion front/assets/translations/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@
},
"common": {
"cancel": "Annuler",
"delete": "Supprimer"
"delete": "Supprimer",
"confirm": "Confirmer"
},
"jobOffer": {
"info": {
"jobOfferList": "Liste des offres d'emploi",
"jobOffer": "Offre d'emploi"
},
"apply": {
"apply": "Postuler",
"applyTitle": "Confirmation de candidature",
"applyConfirm": "Voulez-vous candidater à cette offre ?"
}
},
"profile": {
Expand Down
124 changes: 124 additions & 0 deletions front/src/components/jobOffers/JobOfferContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { useLocales } from 'expo-localization';
import { FC, useCallback } from 'react';
import { Alert, StyleSheet, View } from 'react-native';
import { Button, Text, useTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';

import { JobOffer } from '@/models/entities/jobOffer';
import { usePostApplyJobOfferMutation } from '@/store/api/jobOfferApiSlice';
import i18n from '@/utils/i18n';

/**
* The styles for the JobOfferContent component.
*/
const styles = StyleSheet.create({
button: {
width: 200,
},
container: {
gap: 16,
},
horizontalContainer: {
flexDirection: 'row',
gap: 12,
},
sectionContainer: {
gap: 8,
},
verticalCentered: {
alignItems: 'center',
flexDirection: 'column',
},
});

/**
* The props for the JobOfferContent component.
*/
type JobOfferContentProps = {
/**
* The job offer to display.
*/
jobOffer: JobOffer;
};

/**
* Displays the details about a job offer.
* @constructor
*/
const JobOfferContent: FC<JobOfferContentProps> = ({ jobOffer }) => {
// API calls
const [applyToJobOffer] = usePostApplyJobOfferMutation();

// Hooks
const locales = useLocales();
const theme = useTheme();

// Callbacks
const handleApplyToJobOffer = useCallback(
(jobOffer: JobOffer) => {
Alert.alert(
i18n.t('jobOffer.apply.applyTitle'),
i18n.t('jobOffer.apply.applyConfirm'),
[
{
text: i18n.t('common.cancel'),
style: 'cancel',
},
{
text: i18n.t('common.confirm'),
onPress: () => applyToJobOffer(jobOffer.id),
style: 'destructive',
},
],
);
},
[applyToJobOffer],
);

return (
<View style={styles.container}>
<View style={styles.sectionContainer}>
<Text variant='titleLarge'>{`${jobOffer.title}`}</Text>
<View style={styles.sectionContainer}>
<View style={styles.horizontalContainer}>
<MaterialCommunityIcons
name='calendar'
size={24}
style={{ color: theme.colors.onSurface }}
/>
<Text variant='labelLarge'>
{`${new Date(jobOffer.startDate).toLocaleDateString(
locales[0].languageTag,
)} - ${new Date(jobOffer.endDate).toLocaleDateString(
locales[0].languageTag,
)}`}
</Text>
</View>

<View style={styles.horizontalContainer}>
<MaterialCommunityIcons
name='map-marker'
size={24}
style={{ color: theme.colors.onSurface }}
/>
<Text variant='labelLarge'>{`${jobOffer.geographicArea}`}</Text>
</View>
</View>
</View>

<Text>{`${jobOffer.description}`}</Text>

<View style={styles.verticalCentered}>
<Button
mode={'contained-tonal'}
style={styles.button}
onPress={() => handleApplyToJobOffer(jobOffer)}
>
{i18n.t('jobOffer.apply.apply')}
</Button>
</View>
</View>
);
};

export default JobOfferContent;
67 changes: 37 additions & 30 deletions front/src/components/jobOffers/JobOfferItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useLocales } from 'expo-localization';
import { FC } from 'react';
import { StyleSheet, View } from 'react-native';
import { Text, useTheme } from 'react-native-paper';
import { Text, TouchableRipple, useTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';

import { JobOffer } from '@/models/entities/jobOffer';
Expand All @@ -26,6 +26,11 @@ const styles = StyleSheet.create({
* The props for the JobOfferItem component.
*/
type JobOfferItemProps = {
/**
* The function to call when an a job offer is pressed.
*/
onItemPress?: (jobOffer: JobOffer) => void;

/**
* The job offer to display.
*/
Expand All @@ -36,44 +41,46 @@ type JobOfferItemProps = {
* Displays a job offer.
* @constructor
*/
const JobOfferItem: FC<JobOfferItemProps> = ({ jobOffer }) => {
const JobOfferItem: FC<JobOfferItemProps> = ({ onItemPress, jobOffer }) => {
// Hooks
const locales = useLocales();
const theme = useTheme();

return (
<View style={styles.container}>
<View style={styles.sectionContainer}>
<Text variant='titleLarge'>{`${jobOffer.title}`}</Text>
<Text>{`${jobOffer.description}`}</Text>
</View>

<View style={styles.sectionContainer}>
<View style={styles.horizontalContainer}>
<MaterialCommunityIcons
name='calendar'
size={24}
style={{ color: theme.colors.onSurface }}
/>
<Text variant='labelLarge'>
{`${new Date(jobOffer.startDate).toLocaleDateString(
locales[0].languageTag,
)} - ${new Date(jobOffer.endDate).toLocaleDateString(
locales[0].languageTag,
)}`}
</Text>
<TouchableRipple onPress={() => onItemPress?.(jobOffer)}>
<View style={styles.container}>
<View style={styles.sectionContainer}>
<Text variant='titleLarge'>{`${jobOffer.title}`}</Text>
<Text>{`${jobOffer.description}`}</Text>
</View>

<View style={styles.horizontalContainer}>
<MaterialCommunityIcons
name='map-marker'
size={24}
style={{ color: theme.colors.onSurface }}
/>
<Text variant='labelLarge'>{`${jobOffer.geographicArea}`}</Text>
<View style={styles.sectionContainer}>
<View style={styles.horizontalContainer}>
<MaterialCommunityIcons
name='calendar'
size={24}
style={{ color: theme.colors.onSurface }}
/>
<Text variant='labelLarge'>
{`${new Date(jobOffer.startDate).toLocaleDateString(
locales[0].languageTag,
)} - ${new Date(jobOffer.endDate).toLocaleDateString(
locales[0].languageTag,
)}`}
</Text>
</View>

<View style={styles.horizontalContainer}>
<MaterialCommunityIcons
name='map-marker'
size={24}
style={{ color: theme.colors.onSurface }}
/>
<Text variant='labelLarge'>{`${jobOffer.geographicArea}`}</Text>
</View>
</View>
</View>
</View>
</TouchableRipple>
);
};

Expand Down
20 changes: 9 additions & 11 deletions front/src/components/jobOffers/JobOfferList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,24 @@ const styles = StyleSheet.create({
},
});

/**
* The props for the JobOfferList component.
*/
type JobOfferProps = {
type JobOfferListProps = {
/**
* The job offers to display.
* The function to call when an a job offer is pressed.
*/
onItemPress?: (jobOffer: JobOffer) => void;

/**
* The list of job offers to display.
*/
jobOffers: JobOffer[];
};

/**
* Displays a list of job offers.
* @constructor
*/
const JobOfferList: FC<JobOfferProps> = ({ jobOffers }) => {
const JobOfferList: FC<JobOfferListProps> = ({ onItemPress, jobOffers }) => {
return (
<View>
{jobOffers?.map((jobOffer) => (
<View key={jobOffer.id}>
<JobOfferItem jobOffer={jobOffer} />
<JobOfferItem onItemPress={onItemPress} jobOffer={jobOffer} />
<Divider style={styles.divider} />
</View>
))}
Expand Down
71 changes: 71 additions & 0 deletions front/src/pages/jobOffer/JobOfferListPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useFocusEffect } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { FC, useCallback } from 'react';
import { ScrollView, StyleSheet } from 'react-native';

import JobOfferList from '@/components/jobOffers/JobOfferList';
import { JobOffer } from '@/models/entities/jobOffer';
import { useGetJobOffersQuery } from '@/store/api/jobOfferApiSlice';

import { JobOfferStackParamList } from './JobOfferNav';

/**
* The styles for the JobOfferListPage component.
*/
const styles = StyleSheet.create({
container: {
flex: 1,
},
contentContainer: {
gap: 8,
paddingBottom: 8,
paddingHorizontal: 16,
},
});

/**
* The props for the JobOfferListPage component.
*/
type JobOfferListPageProps = NativeStackScreenProps<
JobOfferStackParamList,
'JobOfferList'
>;

/**
* Displays the page of job offers for the current user.
* @constructor
*/
const JobOfferListPage: FC<JobOfferListPageProps> = ({ navigation }) => {
// API calls
const { data: jobOffers, refetch: refetchJobOffers } = useGetJobOffersQuery();

// Callbacks
const handleJobOfferPress = useCallback(
(jobOffer: JobOffer) => {
navigation.navigate('JobOffer', { id: jobOffer.id });
},
[navigation],
);

// Fetch data from the API when the page is focused
useFocusEffect(
useCallback(() => {
refetchJobOffers();
}, [refetchJobOffers]),
);

if (jobOffers === undefined) {
return null;
}

return (
<ScrollView
style={styles.container}
contentContainerStyle={styles.contentContainer}
>
<JobOfferList onItemPress={handleJobOfferPress} jobOffers={jobOffers} />
</ScrollView>
);
};

export default JobOfferListPage;
Loading