Skip to content
This repository has been archived by the owner on Apr 22, 2024. It is now read-only.

Commit

Permalink
Add announcement creation
Browse files Browse the repository at this point in the history
  • Loading branch information
CarsonHoffman committed May 23, 2019
1 parent a238eab commit 15232c9
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 5 deletions.
41 changes: 41 additions & 0 deletions components/CreateAnnouncementButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { TouchableOpacity, Platform } from 'react-native';
import { withNavigation } from 'react-navigation';
import { connect } from 'react-redux';
import { Ionicons } from '@expo/vector-icons';

import { userHasAnyOfGroups } from '../utils/User';
import { getPlatformSpecificIconName } from '../utils/Icons';

class CreateAnnouncementButton extends React.Component {

render() {
if (!this.props.isAdmin) {
return null;
}

return (
<TouchableOpacity onPress={this._createAnnouncementButtonPress}>
<Ionicons
name={getPlatformSpecificIconName('create')}
style={{ padding: 10 }}
size={20}
/>
</TouchableOpacity>
);
}

_createAnnouncementButtonPress = () => {
this.props.navigation.navigate('CreateAnnouncement');
}

}

function mapStateToProps(state) {
const { auth } = state;
return {
isAdmin: auth.user !== null && userHasAnyOfGroups(auth.user, 'admin'),
};
}

export default withNavigation(connect(mapStateToProps)(CreateAnnouncementButton));
4 changes: 4 additions & 0 deletions config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export default {
// Size of the icons on the tab bar
TAB_NAVIGATOR_ICON_SIZE: 20,

// Window of time (in hours) around the start and end
// of hacking where announcements can be sent
ANNOUNCEMENT_TIME_WINDOW_PADDING: 10,

// A list of colors for the app. It would be nice
// to be able to dynamically fill the list of colors,
// but I guess we have to define them at compile
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
},
"dependencies": {
"expo": "^32.0.0",
"formik": "^1.5.7",
"moment": "^2.24.0",
"react": "16.5.0",
"react-clock": "^2.3.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz",
"react-native-datepicker": "^1.7.2",
"react-native-events-calendar": "github:joshjhargreaves/react-native-event-calendar#3f49b15439bc4ae28f16fbc4d53391fce955c199",
"react-native-paper": "^2.15.2",
"react-native-qrcode-svg": "^5.1.2",
Expand All @@ -19,7 +22,8 @@
"react-navigation-material-bottom-tabs": "^1.0.0",
"react-redux": "^6.0.1",
"redux": "^4.0.1",
"redux-thunk": "^2.3.0"
"redux-thunk": "^2.3.0",
"yup": "^0.27.0"
},
"devDependencies": {
"babel-preset-expo": "^5.0.0"
Expand Down
15 changes: 11 additions & 4 deletions screens/Announcements.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react';
import { View, Text, Header, Platform } from 'react-native';
import { SafeAreaView, createStackNavigator } from 'react-navigation';
import { View, Text } from 'react-native';
import { createStackNavigator } from 'react-navigation';
import { FlatList } from 'react-native';
import Ionicons from '@expo/vector-icons/Ionicons';
import { connect } from 'react-redux';

import Announcement from '../components/Announcement';
import { fetchAnnouncements } from '../actions/Announcements';
import CreateAnnouncementButton from '../components/CreateAnnouncementButton';
import CreateAnnouncementScreen from './CreateAnnouncement';

class AnnouncementsScreen extends React.Component {

Expand Down Expand Up @@ -44,7 +45,13 @@ export default stackNavigator = createStackNavigator({
screen: connect(mapStateToProps)(AnnouncementsScreen),
navigationOptions: {
title: 'Announcements',
headerRight: (<Ionicons name={(Platform.OS === 'ios' ? 'ios' : 'md') + '-create'} style={{ padding: 10 }} size={20} />),
headerRight: <CreateAnnouncementButton />,
}
},
CreateAnnouncement: {
screen: CreateAnnouncementScreen,
navigationOptions: {
title: 'Create Announcement',
}
}
});
212 changes: 212 additions & 0 deletions screens/CreateAnnouncement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import React from 'react';
import { Alert, View, StyleSheet, Picker, TextInput, Text, ScrollView, SafeAreaView } from 'react-native';
import { withNavigation } from 'react-navigation';
import { connect } from 'react-redux';
import { Formik } from 'formik';
import * as yup from 'yup';
import DatePicker from 'react-native-datepicker';
import moment from 'moment';

import Config from '../config/config';
import Endpoints from '../config/endpoints';
import Button from '../components/Button';


class CreateAnnouncementScreen extends React.Component {

state = {
isPostingAnnouncement: false,
};

render() {
// Create a window of Config.ANNOUNCEMENT_TIME_WINDOW_PADDING hours
// on each side of hacking around which announcements can be created.
const datePickerMinDate = new Date(this.props.hackingStartTime.getTime() - 1000 * 60 * 60 * Config.ANNOUNCEMENT_TIME_WINDOW_PADDING);
const datePickerMaxDate = new Date(this.props.hackingEndTime.getTime() + 1000 * 60 * 60 * Config.ANNOUNCEMENT_TIME_WINDOW_PADDING);

// Get the initial time for the date picker. If during the window
// set it to now, if before the window set it to the start,
// and if after the window set it to the end.
const datePickerInitialDate = new Date(Math.min(Math.max(datePickerMinDate, Date.now()), datePickerMaxDate));

return (
<SafeAreaView style={styles.backgroundContainer}>
<Formik
initialValues={{ title: '', body: '', category: 'logistics', broadcastTime: datePickerInitialDate }}
onSubmit={values => this.postAnnouncement(values)}
validationSchema={yup.object().shape({
title: yup
.string()
.required(),
body: yup
.string()
.required(),
})}
>
{({ values, handleChange, setFieldValue, errors, setFieldTouched, touched, isValid, handleSubmit }) => (

<ScrollView>
<View style={styles.formContainer}>
<View style={styles.fieldsContainer}>
<View style={styles.field}>
<Text style={styles.fieldTitle}>Title</Text>
<TextInput
value={values.title}
onChangeText={handleChange('title')}
onBlur={() => setFieldTouched('title')}
autoCapitalize='words'
placeholder='Title'
fontSize={20}
style={styles.textInput}
/>
{touched.title && errors.title &&
<Text style={styles.fieldError}>{errors.title}</Text>
}
</View>
<View style={styles.field}>
<Text style={styles.fieldTitle}>Body</Text>
<TextInput
value={values.body}
onChangeText={handleChange('body')}
placeholder='Body'
multiline={true}
minHeight={60}
maxHeight={60}
fontSize={16}
textAlignVertical='top'
autoCapitalize='sentences'
onBlur={() => setFieldTouched('body')}
style={styles.textInput}
/>
{touched.body && errors.body &&
<Text style={styles.fieldError}>{errors.body}</Text>
}
</View>
<View style={styles.field}>
<Text style={styles.fieldTitle}>Category</Text>
<Picker
selectedValue={values.category}
onValueChange={(itemValue, itemIndex) => setFieldValue('category', itemValue)}
>
<Picker.Item label="Events" color={Config.COLORS.ANNOUNCEMENT_BY_CATEGORY['event']} value="event" />
<Picker.Item label="Food" color={Config.COLORS.ANNOUNCEMENT_BY_CATEGORY['food']} value="food" />
<Picker.Item label="Logistics" color={Config.COLORS.ANNOUNCEMENT_BY_CATEGORY['logistics']} value="logistics" />
<Picker.Item label="Emergency" color={Config.COLORS.ANNOUNCEMENT_BY_CATEGORY['emergency']} value="emergency" />
<Picker.Item label="Sponsored" color={Config.COLORS.ANNOUNCEMENT_BY_CATEGORY['sponsored']} value="sponsored" />
</Picker>
</View>
<View style={styles.field}>
<Text style={styles.fieldTitle}>Broadcast Time</Text>
<View style={{ padding: 5 }}>
<DatePicker
style={{ backgroundColor: '#fff' }}
date={values.broadcastTime}
mode='datetime'
cancelBtnText='Cancel'
confirmBtnText='Confirm'
showIcon={false}
minDate={datePickerMinDate}
maxDate={datePickerMaxDate}
androidMode='spinner'
onDateChange={(dateStr, date) => setFieldValue('broadcastTime', date)}
getDateStr={date => moment(date).format('MMM DD [at] h:mm a')}
/>
</View>
</View>
</View>
<View style={styles.submitButton}>
<Button
title='Create Announcement'
onPress={handleSubmit}
isLoading={this.state.isPostingAnnouncement}
/>
</View>
</View>
</ScrollView>
)}
</Formik>
</SafeAreaView>
);
}

postAnnouncement = (values) => {
this.setState({ isPostingAnnouncement: true });

console.log(JSON.stringify(values));

fetch(Endpoints.ANNOUNCEMENTS, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.props.authToken,
},
body: JSON.stringify(values),
}).then(response => {
if (response.status !== 200) {
throw new Error(response.status);
}

this.setState({ isPostingAnnouncement: false });

Alert.alert('Success', 'Successfully created announcement! Approve the announcement in the web portal to fully post it.');
this.props.navigation.navigate('Announcements');
}).catch(error => {
this.setState({ isPostingAnnouncement: false });

Alert.alert('Error', 'Failed to post announcement: ' + error);
})
}

}

function mapStateToProps(state) {
const { auth, configuration } = state;
return {
authToken: auth.token,
hackingStartTime: new Date(configuration.configuration.start_date),
hackingEndTime: new Date(configuration.configuration.end_date),
};
}

export default withNavigation(connect(mapStateToProps)(CreateAnnouncementScreen));

const styles = StyleSheet.create({
backgroundContainer: {
backgroundColor: '#f6f6fb',
flex: 1,
},
formContainer: {
flex: 1,
marginTop: 25,
},
fieldsContainer: {
flex: 1,
justifyContent: 'flex-start',
},
field: {
marginBottom: 15,
},
fieldTitle: {
color: '#808080',
textTransform: 'uppercase',
marginLeft: 5,
},
fieldError: {
fontSize: 10,
color: 'red',
margin: 5,
},
textInput: {
borderTopWidth: 0.5,
borderBottomWidth: 0.5,
borderColor: '#ddd',
backgroundColor: '#fff',
padding: 5,
marginTop: 10,
},
submitButton: {
alignItems: 'center',
justifyContent: 'center',
margin: 5,
},
});
8 changes: 8 additions & 0 deletions utils/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function userHasAnyOfGroups(user, ...groups) {
for (group of groups) {
if (user.groups.includes(group)) {
return true;
}
}
return false;
}

0 comments on commit 15232c9

Please sign in to comment.