diff --git a/components/CreateAnnouncementButton.js b/components/CreateAnnouncementButton.js
new file mode 100644
index 00000000..62eff8d8
--- /dev/null
+++ b/components/CreateAnnouncementButton.js
@@ -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 (
+
+
+
+ );
+ }
+
+ _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));
\ No newline at end of file
diff --git a/config/config.js b/config/config.js
index dfd7bac3..9d3568ca 100644
--- a/config/config.js
+++ b/config/config.js
@@ -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
diff --git a/package.json b/package.json
index af749d31..144ff8f3 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -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"
diff --git a/screens/Announcements.js b/screens/Announcements.js
index 699c8050..04e36f1e 100644
--- a/screens/Announcements.js
+++ b/screens/Announcements.js
@@ -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 {
@@ -44,7 +45,13 @@ export default stackNavigator = createStackNavigator({
screen: connect(mapStateToProps)(AnnouncementsScreen),
navigationOptions: {
title: 'Announcements',
- headerRight: (),
+ headerRight: ,
}
},
+ CreateAnnouncement: {
+ screen: CreateAnnouncementScreen,
+ navigationOptions: {
+ title: 'Create Announcement',
+ }
+ }
});
\ No newline at end of file
diff --git a/screens/CreateAnnouncement.js b/screens/CreateAnnouncement.js
new file mode 100644
index 00000000..76de4990
--- /dev/null
+++ b/screens/CreateAnnouncement.js
@@ -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 (
+
+ this.postAnnouncement(values)}
+ validationSchema={yup.object().shape({
+ title: yup
+ .string()
+ .required(),
+ body: yup
+ .string()
+ .required(),
+ })}
+ >
+ {({ values, handleChange, setFieldValue, errors, setFieldTouched, touched, isValid, handleSubmit }) => (
+
+
+
+
+
+ Title
+ setFieldTouched('title')}
+ autoCapitalize='words'
+ placeholder='Title'
+ fontSize={20}
+ style={styles.textInput}
+ />
+ {touched.title && errors.title &&
+ {errors.title}
+ }
+
+
+ Body
+ setFieldTouched('body')}
+ style={styles.textInput}
+ />
+ {touched.body && errors.body &&
+ {errors.body}
+ }
+
+
+ Category
+ setFieldValue('category', itemValue)}
+ >
+
+
+
+
+
+
+
+
+ Broadcast Time
+
+ setFieldValue('broadcastTime', date)}
+ getDateStr={date => moment(date).format('MMM DD [at] h:mm a')}
+ />
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+ }
+
+ 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,
+ },
+});
\ No newline at end of file
diff --git a/utils/User.js b/utils/User.js
new file mode 100644
index 00000000..d42f45c9
--- /dev/null
+++ b/utils/User.js
@@ -0,0 +1,8 @@
+export function userHasAnyOfGroups(user, ...groups) {
+ for (group of groups) {
+ if (user.groups.includes(group)) {
+ return true;
+ }
+ }
+ return false;
+}
\ No newline at end of file