🎨 Add prettier and eslint, then format

This commit is contained in:
Nikolai R Kristiansen committed 2019-05-05 12:13:55 +02:00
1 parent ab77f9dea9
commit 148ea8e7c2
33 files changed
+2709 -2118

No files matched your search

+22 -23
View File
@@ -1,31 +1,30 @@
import React, {Component} from 'react';
import {Provider} from 'react-redux';
import {PersistGate} from 'redux-persist/es/integration/react'
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/es/integration/react';
import Config from 'react-native-config';
import {ApolloClient} from 'apollo-client';
import {HttpLink} from 'apollo-link-http';
import {InMemoryCache} from 'apollo-cache-inmemory';
import {ApolloProvider} from "react-apollo";
import Dusken from "./Dusken";
import {store, persistor} from './reduxStore';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloProvider } from 'react-apollo';
import Dusken from './Dusken';
import { store, persistor } from './reduxStore';
const apolloClient = new ApolloClient({
link: new HttpLink({uri: `${Config.DUSKEN_API_URL}/api/graphql/`}),
cache: new InMemoryCache()
link: new HttpLink({ uri: `${Config.DUSKEN_API_URL}/api/graphql/` }),
cache: new InMemoryCache(),
});
export default class App extends Component {
render() {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ApolloProvider client={apolloClient}>
<Dusken />
</ApolloProvider>
</PersistGate>
</Provider>
)
}
render() {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ApolloProvider client={apolloClient}>
<Dusken />
</ApolloProvider>
</PersistGate>
</Provider>
);
}
}
+12 -11
View File
@@ -1,15 +1,16 @@
import React, {Component} from 'react';
import {connect} from "react-redux";
import {Root} from 'native-base';
import DuskenNavigation from "./navigation";
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Root } from 'native-base';
import DuskenNavigation from './navigation';
class DuskenContainer extends Component {
render() {
return (<Root>
<DuskenNavigation/>
</Root>)
}
render() {
return (
<Root>
<DuskenNavigation />
</Root>
);
}
}
export default connect()(DuskenContainer);
export default connect()(DuskenContainer);
+75 -62
View File
@@ -13,103 +13,116 @@ const MEMBERSHIP_CHARGE_SUCCESS = 'MEMBERSHIP_CHARGE_SUCCESS';
const MEMBERSHIP_CHARGE_FAILURE = 'MEMBERSHIP_CHARGE_FAILURE';
function loginRequest() {
return {
type: LOGIN_REQUEST
}
return {
type: LOGIN_REQUEST,
};
}
function loginSuccess(username, token) {
return {
type: LOGIN_SUCCESS,
username,
token
}
return {
type: LOGIN_SUCCESS,
username,
token,
};
}
function loginFailure(loginError) {
return {
type: LOGIN_FAILURE,
loginError
}
return {
type: LOGIN_FAILURE,
loginError,
};
}
function logout() {
return {
type: LOGOUT
}
return {
type: LOGOUT,
};
}
function userDataRequest() {
return {
type: USER_DATA_REQUEST
}
return {
type: USER_DATA_REQUEST,
};
}
function userDataSuccess(data) {
return {
type: USER_DATA_SUCCESS,
data
}
return {
type: USER_DATA_SUCCESS,
data,
};
}
function userDataFailure(userError) {
return {
type: USER_DATA_FAILURE,
userError
}
return {
type: USER_DATA_FAILURE,
userError,
};
}
function registerUserRequest() {
return {
type: REGISTER_USER_REQUEST
}
return {
type: REGISTER_USER_REQUEST,
};
}
function registerUserSuccess(data) {
return {
type: REGISTER_USER_SUCCESS,
data
}
return {
type: REGISTER_USER_SUCCESS,
data,
};
}
function registerUserFailure(registerError) {
return {
type: REGISTER_USER_FAILURE,
registerError
}
return {
type: REGISTER_USER_FAILURE,
registerError,
};
}
function membershipChargeRequest() {
return {
type: MEMBERSHIP_CHARGE_REQUEST
}
return {
type: MEMBERSHIP_CHARGE_REQUEST,
};
}
function membershipChargeSuccess(data) {
return {
type: MEMBERSHIP_CHARGE_SUCCESS,
data
}
return {
type: MEMBERSHIP_CHARGE_SUCCESS,
data,
};
}
function membershipChargeFailure(chargeError) {
return {
type: MEMBERSHIP_CHARGE_FAILURE,
chargeError
}
return {
type: MEMBERSHIP_CHARGE_FAILURE,
chargeError,
};
}
export {
LOGIN_REQUEST, loginRequest,
LOGIN_SUCCESS, loginSuccess,
LOGIN_FAILURE, loginFailure,
LOGOUT, logout,
USER_DATA_REQUEST, userDataRequest,
USER_DATA_SUCCESS, userDataSuccess,
USER_DATA_FAILURE, userDataFailure,
REGISTER_USER_REQUEST, registerUserRequest,
REGISTER_USER_SUCCESS, registerUserSuccess,
REGISTER_USER_FAILURE, registerUserFailure,
MEMBERSHIP_CHARGE_REQUEST, membershipChargeRequest,
MEMBERSHIP_CHARGE_SUCCESS, membershipChargeSuccess,
MEMBERSHIP_CHARGE_FAILURE, membershipChargeFailure,
};
LOGIN_REQUEST,
loginRequest,
LOGIN_SUCCESS,
loginSuccess,
LOGIN_FAILURE,
loginFailure,
LOGOUT,
logout,
USER_DATA_REQUEST,
userDataRequest,
USER_DATA_SUCCESS,
userDataSuccess,
USER_DATA_FAILURE,
userDataFailure,
REGISTER_USER_REQUEST,
registerUserRequest,
REGISTER_USER_SUCCESS,
registerUserSuccess,
REGISTER_USER_FAILURE,
registerUserFailure,
MEMBERSHIP_CHARGE_REQUEST,
membershipChargeRequest,
MEMBERSHIP_CHARGE_SUCCESS,
membershipChargeSuccess,
MEMBERSHIP_CHARGE_FAILURE,
membershipChargeFailure,
};
+135 -129
View File
@@ -1,148 +1,154 @@
import Config from 'react-native-config';
import {
loginFailure, loginRequest, loginSuccess,
registerUserFailure, registerUserRequest, registerUserSuccess,
userDataFailure, userDataRequest, userDataSuccess,
membershipChargeFailure, membershipChargeRequest, membershipChargeSuccess,
} from "./actions";
import { fetchWithTimeout } from "./utils";
loginFailure,
loginRequest,
loginSuccess,
registerUserFailure,
registerUserRequest,
registerUserSuccess,
userDataFailure,
userDataRequest,
userDataSuccess,
membershipChargeFailure,
membershipChargeRequest,
membershipChargeSuccess,
} from './actions';
import { fetchWithTimeout } from './utils';
export function requestLogin(username, password) {
return (dispatch) => {
// We are now logging in
dispatch(loginRequest());
return (dispatch) => {
// We are now logging in
dispatch(loginRequest());
return fetchWithTimeout(`${Config.DUSKEN_API_URL}/auth/obtain-token/`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'username': username,
'password': password
})
})
.then(response => response.json().then(json => ({ json, response })))
.then(({json, response}) => {
if (response.ok === false) {
return Promise.reject(json)
}
return json
})
.then(
data => {
dispatch(loginSuccess(username, data.token))
},
data => {
console.log(data);
dispatch(loginFailure(data || {'non_field_errors': ['Kunne ikke logge inn, prøv igjen']}))
}
)
}
return fetchWithTimeout(`${Config.DUSKEN_API_URL}/auth/obtain-token/`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password,
}),
})
.then((response) => response.json().then((json) => ({ json, response })))
.then(({ json, response }) => {
if (response.ok === false) {
return Promise.reject(json);
}
return json;
})
.then(
(data) => {
dispatch(loginSuccess(username, data.token));
},
(data) => {
console.log(data);
dispatch(loginFailure(data || { non_field_errors: ['Kunne ikke logge inn, prøv igjen'] }));
}
);
};
}
export function requestUserData(auth_token) {
return (dispatch) => {
// We are now fetching user data
dispatch(userDataRequest());
return (dispatch) => {
// We are now fetching user data
dispatch(userDataRequest());
return fetchWithTimeout(`${Config.DUSKEN_API_URL}/api/me/`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Token ${auth_token}`
}
})
.then(response => response.json().then(json => ({ json, response })))
.then(({json, response}) => {
if (response.ok === false) {
return Promise.reject(json)
}
return json
})
.then(
data => {
dispatch(userDataSuccess(data))
},
data => {
dispatch(userDataFailure(data || {'non_field_errors': ['Kunne ikke hente brukerdata']}))
}
)
}
return fetchWithTimeout(`${Config.DUSKEN_API_URL}/api/me/`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${auth_token}`,
},
})
.then((response) => response.json().then((json) => ({ json, response })))
.then(({ json, response }) => {
if (response.ok === false) {
return Promise.reject(json);
}
return json;
})
.then(
(data) => {
dispatch(userDataSuccess(data));
},
(data) => {
dispatch(userDataFailure(data || { non_field_errors: ['Kunne ikke hente brukerdata'] }));
}
);
};
}
export function requestRegisterUser(firstName, lastName, email, phoneNumber, password) {
return (dispatch) => {
dispatch(registerUserRequest());
return (dispatch) => {
dispatch(registerUserRequest());
return fetchWithTimeout(`${Config.DUSKEN_API_URL}/api/user/register/`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
'first_name': firstName,
'last_name': lastName,
'email': email,
'phone_number': phoneNumber,
'password': password
})
})
.then(response => response.json().then(json => ({ json, response })))
.then(({json, response}) => {
if (response.ok === false) {
return Promise.reject(json)
}
return json
})
.then(
data => {
dispatch(registerUserSuccess(data))
},
data => {
dispatch(registerUserFailure(data || {'non_field_errors': ['Kunne ikke registrere bruker, prøv igjen']}))
}
)
}
return fetchWithTimeout(`${Config.DUSKEN_API_URL}/api/user/register/`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
first_name: firstName,
last_name: lastName,
email,
phone_number: phoneNumber,
password,
}),
})
.then((response) => response.json().then((json) => ({ json, response })))
.then(({ json, response }) => {
if (response.ok === false) {
return Promise.reject(json);
}
return json;
})
.then(
(data) => {
dispatch(registerUserSuccess(data));
},
(data) => {
dispatch(registerUserFailure(data || { non_field_errors: ['Kunne ikke registrere bruker, prøv igjen'] }));
}
);
};
}
export function requestMembershipCharge(auth_token, stripe_token, membership_type) {
return (dispatch) => {
dispatch(membershipChargeRequest());
return (dispatch) => {
dispatch(membershipChargeRequest());
return fetchWithTimeout(`${Config.DUSKEN_API_URL}/api/membership/charge/`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Token ${auth_token}`
},
body: JSON.stringify({
'stripe_token': stripe_token,
'membership_type': membership_type,
'payment_method': 'app',
})
})
.then(response => response.json().then(json => ({ json, response })))
.then(({json, response}) => {
if (response.ok === false) {
return Promise.reject(json)
}
return json
})
.then(
data => {
dispatch(membershipChargeSuccess(data))
},
data => {
dispatch(membershipChargeFailure(data['detail'] || 'Kunne ikke belaste bankkortet, prøv igjen'))
}
)
}
return fetchWithTimeout(`${Config.DUSKEN_API_URL}/api/membership/charge/`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${auth_token}`,
},
body: JSON.stringify({
stripe_token,
membership_type,
payment_method: 'app',
}),
})
.then((response) => response.json().then((json) => ({ json, response })))
.then(({ json, response }) => {
if (response.ok === false) {
return Promise.reject(json);
}
return json;
})
.then(
(data) => {
dispatch(membershipChargeSuccess(data));
},
(data) => {
dispatch(membershipChargeFailure(data.detail || 'Kunne ikke belaste bankkortet, prøv igjen'));
}
);
};
}
+172 -137
View File
@@ -1,156 +1,191 @@
import React, { Component } from 'react';
import {Text, ScrollView, View, Platform, StyleSheet, Image, Linking, TouchableOpacity} from "react-native";
import { Text, ScrollView, View, Platform, StyleSheet, Image, Linking, TouchableOpacity } from 'react-native';
// import HTMLView from 'react-native-htmlview';
import {Button, Text as NBText, Icon} from 'native-base';
import theme from "../../theme";
import { Button, Text as NBText, Icon } from 'native-base';
import theme from '../../theme';
const CHATEAU_NEUF_ADDRESS = 'Det Norske Studentersamfund, Slemdalsveien 15, 0369 Oslo';
export default class About extends Component {
/* FIXME: Values here are pretty hardcoded. Should fetch config from a JSON-file or API */
/* FIXME: Values here are pretty hardcoded. Should fetch config from a JSON-file or API */
openFacebook() {
/* Ref: https://stackoverflow.com/questions/4810803/open-facebook-page-from-android-app */
const url = Platform.select({
ios: 'fb://page/?id=23664032291',
android: 'fb://page/23664032291/'
});
Linking.canOpenURL(url).then(supported => {
if (!supported) {
return Linking.openURL('https://www.facebook.com/studentersamfundet/');
} else {
return Linking.openURL(url);
}
}).catch(err => console.error('An error occurred', err));
}
openInstagram() {
/* Ref: */
const url = Platform.select({
ios: 'instagram://user?username=studentersamfundet',
android: 'http://instagram.com/_u/studentersamfundet'
});
openFacebook() {
/* Ref: https://stackoverflow.com/questions/4810803/open-facebook-page-from-android-app */
const url = Platform.select({
ios: 'fb://page/?id=23664032291',
android: 'fb://page/23664032291/',
});
Linking.canOpenURL(url)
.then((supported) => {
if (!supported) {
return Linking.openURL('https://www.facebook.com/studentersamfundet/');
}
return Linking.openURL(url);
})
.catch((err) => console.error('An error occurred', err));
}
Linking.canOpenURL(url).then(supported => {
if (!supported) {
return Linking.openURL('https://www.instagram.com/studentersamfundet/');
} else {
return Linking.openURL(url);
}
}).catch(err => console.error('An error occurred', err));
}
openInstagram() {
/* Ref: */
const url = Platform.select({
ios: 'instagram://user?username=studentersamfundet',
android: 'http://instagram.com/_u/studentersamfundet',
});
openMap() {
const url = Platform.select({
ios: `comgooglemaps://?q=${CHATEAU_NEUF_ADDRESS}`,
android: `https://www.google.com/maps/search/?api=1&query=${CHATEAU_NEUF_ADDRESS}`
});
Linking.canOpenURL(url).then(supported => {
if (!supported) {
return Linking.openURL(`https://www.google.com/maps/search/?api=1&query=${CHATEAU_NEUF_ADDRESS}`);
} else {
return Linking.openURL(url);
}
}).catch(err => console.error('An error occurred', err));
}
Linking.canOpenURL(url)
.then((supported) => {
if (!supported) {
return Linking.openURL('https://www.instagram.com/studentersamfundet/');
}
return Linking.openURL(url);
})
.catch((err) => console.error('An error occurred', err));
}
render() {
return (
<ScrollView>
<View style={styles.card}>
<Text style={[styles.header, {marginTop: 8}]}>Det Norske Studentersamfund</Text>
<TouchableOpacity onPress={() => Linking.openURL('https://www.studentersamfundet.no/')}>
<Image source={{uri: 'https://galtinn.neuf.no/static/dist/images/logo.png'}} style={styles.logo} resizeMode="contain" />
</TouchableOpacity>
<Text style={styles.header}>Følg oss på</Text>
<View style={[styles.section, {flex: 1, flexDirection: 'row'}]}>
<TouchableOpacity style={styles.someBtn} onPress={this.openFacebook}>
<Icon name="logo-facebook" style={styles.largeIcon} /><NBText>Facebook</NBText>
</TouchableOpacity>
<TouchableOpacity style={styles.someBtn} onPress={this.openInstagram}>
<Icon name="logo-instagram" style={styles.largeIcon} /><NBText>Instagram</NBText>
</TouchableOpacity>
</View>
openMap() {
const url = Platform.select({
ios: `comgooglemaps://?q=${CHATEAU_NEUF_ADDRESS}`,
android: `https://www.google.com/maps/search/?api=1&query=${CHATEAU_NEUF_ADDRESS}`,
});
Linking.canOpenURL(url)
.then((supported) => {
if (!supported) {
return Linking.openURL(`https://www.google.com/maps/search/?api=1&query=${CHATEAU_NEUF_ADDRESS}`);
}
return Linking.openURL(url);
})
.catch((err) => console.error('An error occurred', err));
}
<Text style={styles.header}>Adresse</Text>
<Text style={[styles.paragraph, {textAlign: 'center'}]}>Det Norske Studentersamfund{"\n"}Slemdalsveien 15, 0369 Oslo</Text>
<View style={styles.section}>
<Button iconLeft onPress={this.openMap}>
<Icon name="map" style={styles.icon} /><NBText> Vis i kart</NBText>
</Button>
</View>
<Text style={styles.header}>Åpningstider</Text>
<View style={styles.section}>
<Button iconLeft onPress={() => Linking.openURL('https://studentersamfundet.no/aapningstider/')}>
<Icon name="time" style={styles.icon} /><NBText>Vis Åpningstider</NBText>
</Button>
</View>
<Text style={styles.header}>Ta kontakt</Text>
<View style={[styles.section, {flex: 1, flexDirection: 'row'}]}>
<View>
<Button iconLeft onPress={() => Linking.openURL('tel:+4794430002')} style={{marginHorizontal: 10}}>
<Icon name="call" style={styles.icon} /><NBText>Ring</NBText>
</Button>
</View>
<View>
<Button iconLeft onPress={() => Linking.openURL('mailto:ledelsen@studentersamfundet.no')} style={{marginHorizontal: 10}}>
<Icon name="mail" style={styles.icon} /><NBText>Send e-post</NBText>
</Button>
</View>
</View>
render() {
return (
<ScrollView>
<View style={styles.card}>
<Text style={[styles.header, { marginTop: 8 }]}>Det Norske Studentersamfund</Text>
<TouchableOpacity onPress={() => Linking.openURL('https://www.studentersamfundet.no/')}>
<Image
source={{ uri: 'https://galtinn.neuf.no/static/dist/images/logo.png' }}
style={styles.logo}
resizeMode="contain"
/>
</TouchableOpacity>
<Text style={styles.header}>Følg oss på</Text>
<View style={[styles.section, { flex: 1, flexDirection: 'row' }]}>
<TouchableOpacity style={styles.someBtn} onPress={this.openFacebook}>
<Icon name="logo-facebook" style={styles.largeIcon} />
<NBText>Facebook</NBText>
</TouchableOpacity>
<TouchableOpacity style={styles.someBtn} onPress={this.openInstagram}>
<Icon name="logo-instagram" style={styles.largeIcon} />
<NBText>Instagram</NBText>
</TouchableOpacity>
</View>
<Text style={styles.header}>Chateau Neuf</Text>
<Image source={{uri: 'https://studentersamfundet.no/wp-content/uploads/2015/04/26_KUL_Chateauneuf-1280x720.jpg'}} style={styles.featuredImage} resizeMode="contain" />
<Text style={styles.paragraph}>Studentene i Oslo har sitt naturlige tilholdssted på Det Norske Studentersamfund, i hyggelige lokaler på Chateau Neuf øverst på Majorstuen. Her er det åpent alle dager unntatt søndag, og hver dag skjer det en rekke ting i våre mange arrangementslokaler. Enten man ønsker en tur i baren, på kafé, på debatt, på konsert, teater eller kino, har man muligheten på Chateau Neuf.</Text>
<Text style={styles.paragraph}>Chateau Neuf er et frivillig drevet studenthus. Studenter over alt i Oslo kan komme hit og få nyttig og spennende erfaring med å drive et av Oslos største kulturhus. Her kan du stå i bar, sette lys og lyd, jobbe med markedsføring, produsere konserter, bli kjent med hundrevis av bra mennesker og få minner for livet.</Text>
<Text style={styles.paragraph}>Alle studentforeninger i Oslo kan arrangere gratis på Chateau Neuf. Skal du arrangere en fest, sette en konsert, holde et foredrag eller gjennomføre et møte, har vi lokaler som kan brukes til dette.</Text>
<Text style={styles.header}>Adresse</Text>
<Text style={[styles.paragraph, { textAlign: 'center' }]}>
Det Norske Studentersamfund{'\n'}Slemdalsveien 15, 0369 Oslo
</Text>
<View style={styles.section}>
<Button iconLeft onPress={this.openMap}>
<Icon name="map" style={styles.icon} />
<NBText> Vis i kart</NBText>
</Button>
</View>
<Text style={styles.header}>Åpningstider</Text>
<View style={styles.section}>
<Button iconLeft onPress={() => Linking.openURL('https://studentersamfundet.no/aapningstider/')}>
<Icon name="time" style={styles.icon} />
<NBText>Vis Åpningstider</NBText>
</Button>
</View>
<Text style={styles.header}>Ta kontakt</Text>
<View style={[styles.section, { flex: 1, flexDirection: 'row' }]}>
<View>
<Button iconLeft onPress={() => Linking.openURL('tel:+4794430002')} style={{ marginHorizontal: 10 }}>
<Icon name="call" style={styles.icon} />
<NBText>Ring</NBText>
</Button>
</View>
<View>
<Button
iconLeft
onPress={() => Linking.openURL('mailto:ledelsen@studentersamfundet.no')}
style={{ marginHorizontal: 10 }}
>
<Icon name="mail" style={styles.icon} />
<NBText>Send e-post</NBText>
</Button>
</View>
</View>
{/*<HTMLView style={styles.content} stylesheet={HTMLStyles} value={this.props.openingHours}/>*/}
</View>
</ScrollView>
)
}
<Text style={styles.header}>Chateau Neuf</Text>
<Image
source={{ uri: 'https://studentersamfundet.no/wp-content/uploads/2015/04/26_KUL_Chateauneuf-1280x720.jpg' }}
style={styles.featuredImage}
resizeMode="contain"
/>
<Text style={styles.paragraph}>
Studentene i Oslo har sitt naturlige tilholdssted på Det Norske Studentersamfund, i hyggelige lokaler på
Chateau Neuf øverst på Majorstuen. Her er det åpent alle dager unntatt søndag, og hver dag skjer det en
rekke ting i våre mange arrangementslokaler. Enten man ønsker en tur i baren, på kafé, på debatt, på
konsert, teater eller kino, har man muligheten på Chateau Neuf.
</Text>
<Text style={styles.paragraph}>
Chateau Neuf er et frivillig drevet studenthus. Studenter over alt i Oslo kan komme hit og få nyttig og
spennende erfaring med å drive et av Oslos største kulturhus. Her kan du stå i bar, sette lys og lyd, jobbe
med markedsføring, produsere konserter, bli kjent med hundrevis av bra mennesker og få minner for livet.
</Text>
<Text style={styles.paragraph}>
Alle studentforeninger i Oslo kan arrangere gratis på Chateau Neuf. Skal du arrangere en fest, sette en
konsert, holde et foredrag eller gjennomføre et møte, har vi lokaler som kan brukes til dette.
</Text>
{/* <HTMLView style={styles.content} stylesheet={HTMLStyles} value={this.props.openingHours}/> */}
</View>
</ScrollView>
);
}
}
const styles = StyleSheet.create({
header: {
fontSize: 20,
fontWeight: 'bold',
color: '#000',
textAlign: 'center',
},
paragraph: {
fontSize: 16,
color: '#000',
paddingBottom: 10,
lineHeight: 25,
},
card: theme.card,
featuredImage: {
height: 230,
width: null,
},
logo: {
height: 128,
width: null,
marginBottom: 10,
},
icon: {
fontSize: 16,
},
largeIcon: {
fontSize: 40,
},
section: {
marginTop: 8,
marginBottom: 20,
alignSelf: 'center',
},
someBtn: {
flex: 1,
alignItems: 'center',
}
header: {
fontSize: 20,
fontWeight: 'bold',
color: '#000',
textAlign: 'center',
},
paragraph: {
fontSize: 16,
color: '#000',
paddingBottom: 10,
lineHeight: 25,
},
card: theme.card,
featuredImage: {
height: 230,
width: null,
},
logo: {
height: 128,
width: null,
marginBottom: 10,
},
icon: {
fontSize: 16,
},
largeIcon: {
fontSize: 40,
},
section: {
marginTop: 8,
marginBottom: 20,
alignSelf: 'center',
},
someBtn: {
flex: 1,
alignItems: 'center',
},
});
// const HTMLStyles = StyleSheet.create({
@@ -165,4 +200,4 @@ const styles = StyleSheet.create({
// marginTop: -20,
// marginBottom: -30,
// },
// });
// });
+7 -40
View File
@@ -1,45 +1,12 @@
import {connect} from "react-redux";
import React, {Component} from 'react';
import Config from 'react-native-config';
import { connect } from 'react-redux';
import React, { Component } from 'react';
import About from "./About";
import About from './About';
class AboutContainer extends Component {
// constructor(props) {
// super(props);
// this.state = {
// openingHours: '',
// loading: false,
// error: null,
// };
// }
//
// componentDidMount() {
// this.fetchOpeningHours();
// }
//
// fetchOpeningHours() {
// const url = `${Config.EVENT_API_URL}/wp-json/wp/v2/pages/?slug=aapningstider`;
// this.setState({ loading: true });
//
// fetch(url)
// .then(res => res.json())
// .then(res => {
// this.setState({
// openingHours: res ? res[0].content.rendered : '',
// error: res.error || null,
// loading: false,
// });
//
// })
// .catch(error => {
// this.setState({ error, loading: false });
// });
// }
render() {
return <About {...this.state} />;
}
render() {
return <About {...this.state} />;
}
}
export default connect()(AboutContainer);
export default connect()(AboutContainer);
+292 -272
View File
@@ -1,305 +1,325 @@
import React, { Component } from 'react';
import {Linking, StyleSheet, ScrollView, Text, Platform, View, Image, TouchableOpacity} from "react-native";
import { Linking, StyleSheet, ScrollView, Text, Platform, View, Image, TouchableOpacity } from 'react-native';
import HTMLView from 'react-native-htmlview';
import moment from "moment";
import {Icon} from "native-base";
import theme from "../../theme";
import moment from 'moment';
import { Icon } from 'native-base';
import theme from '../../theme';
moment.locale('nb');
const CHATEAU_NEUF_ADDRESS = 'Chateau Neuf, Slemdalsveien 15, 0369 Oslo';
export default class EventDetail extends Component {
_formatTime(time) {
return moment(time).format('HH:mm');
}
_formatTime(time) {
return moment(time).format('HH:mm');
_formatMonth(time) {
return moment(time).format('MMM');
}
_formatDay(time) {
return moment(time).format('DD.');
}
_formatLocalDay(time) {
return moment(time).format('ddd');
}
_formatYear(time) {
return moment(time).format('YYYY');
}
_formatRelative(time) {
return moment(time).fromNow();
}
_formatLocation() {
if (this.props.item.venue !== 'Annetsteds') {
return CHATEAU_NEUF_ADDRESS;
}
return 'Ukjent adresse';
}
_formatTicketText() {
/* FIXME: This should be handled by API */
const { item } = this.props;
let reg_kr = '';
let mem_kr = '';
if (item.price_regular === '0') {
item.price_regular = 'Gratis';
}
if (item.price_member === '0') {
item.price_member = 'Gratis';
}
if (item.price_regular !== 'Gratis') {
reg_kr = ' kr';
}
if (item.price_member !== 'Gratis') {
mem_kr = ' kr';
}
_formatMonth(time) {
return moment(time).format('MMM');
let text = '';
if (item.price_regular && item.price_member) {
text = `Pris: ${item.price_regular}${reg_kr} / Medlemmer: ${item.price_member}${mem_kr}`;
} else if (item.price_regular) {
text = `Pris: ${item.price_regular}${reg_kr}`;
} else if (item.price_member) {
text = `Pris (Medlemmer): ${item.price_member}${mem_kr}`;
} else {
return '';
}
return text;
}
_formatDay(time) {
return moment(time).format('DD.');
}
_formatLocalDay(time) {
return moment(time).format('ddd')
}
onTicketUrlPress(link) {
Linking.openURL(link);
}
_formatYear(time) {
return moment(time).format('YYYY');
onFacebookUrlPress(link) {
const fbEventPattern = /https?:\/\/www\.facebook\.com\/events\/(\d+)\/?/i;
const matches = fbEventPattern.exec(link);
let iosURL = link;
if (matches && matches.length) {
iosURL = `fb://event/?id=${matches[1]}`;
}
_formatRelative(time) {
return moment(time).fromNow();
}
_formatLocation() {
if(this.props.item.venue !== 'Annetsteds') {
return CHATEAU_NEUF_ADDRESS;
const url = Platform.select({
ios: iosURL,
android: link,
});
Linking.canOpenURL(url)
.then((supported) => {
if (!supported) {
return Linking.openURL(link);
}
return 'Ukjent adresse'
return Linking.openURL(url);
})
.catch((err) => console.error('An error occurred', err));
}
onLocationPress(venue) {
if (venue !== 'Annetsteds') {
Linking.openURL(`https://www.google.com/maps/dir/?api=1&destination=${CHATEAU_NEUF_ADDRESS}`);
}
}
showPriceAndTicket() {
const { item } = this.props;
const text = this._formatTicketText();
if (text === '') {
return;
}
_formatTicketText() {
/* FIXME: This should be handled by API */
const {item} = this.props;
let reg_kr = '';
let mem_kr = '';
if (item.price_regular === '0') {
item.price_regular = 'Gratis'
}
if (item.price_member === '0') {
item.price_member = 'Gratis'
}
if (item.price_regular !== 'Gratis') {
reg_kr = " kr"
}
if (item.price_member !== 'Gratis') {
mem_kr = " kr"
}
let text = '';
if (item.price_regular && item.price_member) {
text = `Pris: ${item.price_regular}${reg_kr} / Medlemmer: ${item.price_member}${mem_kr}`;
}
else if (item.price_regular) {
text = `Pris: ${item.price_regular}${reg_kr}`;
}
else if (item.price_member) {
text = `Pris (Medlemmer): ${item.price_member}${mem_kr}`;
} else {
return '';
}
return text;
if (item.ticket_url) {
return (
<TouchableOpacity style={styles.metaInner} onPress={() => this.onTicketUrlPress(item.ticket_url)}>
<View style={styles.metaIcon}>
<Icon name="card" style={styles.icons} />
</View>
<View style={{ flexDirection: 'column' }}>
<Text style={styles.metaText}>{text}</Text>
<Text style={styles.metaSubtitle}>Kjøp billett</Text>
</View>
</TouchableOpacity>
);
}
onTicketUrlPress(link) {
Linking.openURL(link);
return (
<View style={styles.metaInner}>
<View style={styles.metaIcon}>
<Icon name="card" style={styles.icons} />
</View>
<View style={{ flexDirection: 'column' }}>
<Text style={styles.metaText}>{text}</Text>
<Text style={styles.metaSubtitle}>Billett i døra</Text>
</View>
</View>
);
}
showFacebutton() {
const { item } = this.props;
if (!item.facebook_url) {
return;
}
onFacebookUrlPress(link) {
const fbEventPattern = /https?:\/\/www\.facebook\.com\/events\/(\d+)\/?/i;
const matches = fbEventPattern.exec(link);
let iosURL = link;
if( matches && matches.length ) {
iosURL = 'fb://event/?id=' + matches[1];
}
const url = Platform.select({
ios: iosURL,
android: link
});
Linking.canOpenURL(url).then(supported => {
if (!supported) {
return Linking.openURL(link);
} else {
return Linking.openURL(url);
}
}).catch(err => console.error('An error occurred', err));
return (
<TouchableOpacity onPress={() => this.onFacebookUrlPress(item.facebook_url)} style={styles.metaInner}>
<View style={[styles.metaIcon, { marginTop: 0 }]}>
<Icon name="logo-facebook" style={styles.icons} />
</View>
<View>
<Text style={[styles.metaText, { paddingTop: 2 }]}>På Facebook</Text>
</View>
</TouchableOpacity>
);
}
showImage() {
const { item } = this.props;
if (item.thumbnail.medium_large) {
return <Image source={{ uri: item.thumbnail.medium_large }} style={styles.image} />;
}
}
onLocationPress(venue) {
if( venue !== 'Annetsteds' ) {
Linking.openURL(`https://www.google.com/maps/dir/?api=1&destination=${CHATEAU_NEUF_ADDRESS}`);
}
showYear() {
const { item } = this.props;
if (moment().year() !== moment(item.start_time).year()) {
return <Text style={styles.year}>{this._formatYear(item.start_time)}</Text>;
}
}
showPriceAndTicket() {
const {item} = this.props;
const text = this._formatTicketText();
render() {
const { item } = this.props;
if(text === '') {
return;
}
if(item.ticket_url) {
return (
<TouchableOpacity style={styles.metaInner} onPress={() => this.onTicketUrlPress(item.ticket_url)}>
<View style={styles.metaIcon}><Icon name="card" style={styles.icons} /></View>
<View style={{flexDirection: 'column'}}>
<Text style={styles.metaText}>{text}</Text>
<Text style={styles.metaSubtitle}>Kjøp billett</Text>
</View>
</TouchableOpacity>);
}
return (
<View style={styles.metaInner}>
<View style={styles.metaIcon}><Icon name="card" style={styles.icons} /></View>
<View style={{flexDirection: 'column'}}>
<Text style={styles.metaText}>{text}</Text>
<Text style={styles.metaSubtitle}>Billett i døra</Text>
</View>
</View>)
}
showFacebutton() {
const {item} = this.props;
if (!item.facebook_url) { return;}
return (
<TouchableOpacity onPress={() => this.onFacebookUrlPress(item.facebook_url)} style={styles.metaInner}>
<View style={[styles.metaIcon, {marginTop: 0}]}><Icon name="logo-facebook" style={styles.icons} /></View>
<View>
<Text style={[styles.metaText, {paddingTop: 2}]}>På Facebook</Text>
</View>
</TouchableOpacity>
);
}
showImage() {
const {item} = this.props;
if (item.thumbnail.medium_large) {
return <Image source={{uri: item.thumbnail.medium_large}} style={styles.image} />;
}
}
showYear() {
const {item} = this.props;
if(moment().year() !== moment(item.start_time).year()) {
return <Text style={styles.year}>{this._formatYear(item.start_time)}</Text>
}
}
render() {
const {item} = this.props;
return (
<ScrollView>
{this.showImage()}
<View style={styles.card}>
{/* Date and title */}
<View style={styles.dateAndTitle}>
<View style={styles.dateContainer}>
<View style={[styles.card, styles.dateCard]}>
<Text style={styles.day}>{this._formatDay(item.start_time)}</Text>
<Text style={styles.month}>{this._formatMonth(item.start_time)}</Text>
{this.showYear()}
</View>
</View>
<View style={styles.titleContainer}>
<Text style={styles.title} numberOfLines={2}>{item.title.decoded}</Text>
</View>
</View>
{/* Time and calendar */}
<TouchableOpacity style={styles.metaInner}>
<View style={styles.metaIcon}><Icon name="time" style={styles.icons} /></View>
<View style={{flexDirection: 'column'}}>
<Text style={styles.metaText}>{this._formatLocalDay(item.start_time)} kl. {this._formatTime(item.start_time)} - {this._formatTime(item.end_time)}</Text>
<Text style={styles.metaSubtitle}>{this._formatRelative(item.start_time)}</Text>
</View>
</TouchableOpacity>
{/* Location and maps */}
<TouchableOpacity style={styles.metaInner} onPress={() => this.onLocationPress(item.venue)}>
<View style={[styles.metaIcon, {paddingLeft: 1}]}><Icon name="pin" style={styles.icons} /></View>
<View style={{flexDirection: 'column'}}>
<Text style={styles.metaText}>{item.venue}</Text>
<Text style={styles.metaSubtitle} numberOfLines={1}>{this._formatLocation()}</Text>
</View>
</TouchableOpacity>
{this.showPriceAndTicket()}
{this.showFacebutton()}
<HTMLView style={styles.content} stylesheet={HTMLStyles} value={item.content.rendered}/>
</View>
</ScrollView>
)
}
return (
<ScrollView>
{this.showImage()}
<View style={styles.card}>
{/* Date and title */}
<View style={styles.dateAndTitle}>
<View style={styles.dateContainer}>
<View style={[styles.card, styles.dateCard]}>
<Text style={styles.day}>{this._formatDay(item.start_time)}</Text>
<Text style={styles.month}>{this._formatMonth(item.start_time)}</Text>
{this.showYear()}
</View>
</View>
<View style={styles.titleContainer}>
<Text style={styles.title} numberOfLines={2}>
{item.title.decoded}
</Text>
</View>
</View>
{/* Time and calendar */}
<TouchableOpacity style={styles.metaInner}>
<View style={styles.metaIcon}>
<Icon name="time" style={styles.icons} />
</View>
<View style={{ flexDirection: 'column' }}>
<Text style={styles.metaText}>
{this._formatLocalDay(item.start_time)} kl. {this._formatTime(item.start_time)} -{' '}
{this._formatTime(item.end_time)}
</Text>
<Text style={styles.metaSubtitle}>{this._formatRelative(item.start_time)}</Text>
</View>
</TouchableOpacity>
{/* Location and maps */}
<TouchableOpacity style={styles.metaInner} onPress={() => this.onLocationPress(item.venue)}>
<View style={[styles.metaIcon, { paddingLeft: 1 }]}>
<Icon name="pin" style={styles.icons} />
</View>
<View style={{ flexDirection: 'column' }}>
<Text style={styles.metaText}>{item.venue}</Text>
<Text style={styles.metaSubtitle} numberOfLines={1}>
{this._formatLocation()}
</Text>
</View>
</TouchableOpacity>
{this.showPriceAndTicket()}
{this.showFacebutton()}
<HTMLView style={styles.content} stylesheet={HTMLStyles} value={item.content.rendered} />
</View>
</ScrollView>
);
}
}
const styles = StyleSheet.create({
dateAndTitle: {
flexDirection: 'row',
marginBottom: 8
},
titleContainer: {
paddingLeft: 10,
justifyContent: 'center',
flex: 4,
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#000',
},
card: theme.card,
image: {
height: 260,
width: null
},
time: {
fontSize: 14,
color: '#666'
},
metaInner: {
flexDirection: 'row',
marginVertical: 8,
},
metaIcon: {
width: 16,
paddingTop: 3,
marginHorizontal: 8,
marginVertical: 8,
},
metaText: {
color: '#000',
},
metaSubtitle: {
color: '#666',
},
content: {
borderTopWidth: StyleSheet.hairlineWidth,
borderColor: '#ccc',
marginTop: 8,
paddingTop: 10,
},
dateContainer: {
flex: 1,
justifyContent: 'center',
},
dateCard: {
margin: 0,
padding: 0,
paddingBottom: 6,
width: 62,
},
day: {
paddingTop: 5,
fontSize: 20,
color: '#000',
textAlign: 'center',
},
month: {
textAlign: 'center',
paddingHorizontal: 16,
},
year: {
fontSize: 9,
color: '#fff',
backgroundColor: '#f58220',
textAlign: 'center',
paddingRight: 20,
paddingBottom: 3,
paddingLeft: 20,
paddingTop: 3,
},
icons: {
fontSize: 16,
},
dateAndTitle: {
flexDirection: 'row',
marginBottom: 8,
},
titleContainer: {
paddingLeft: 10,
justifyContent: 'center',
flex: 4,
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#000',
},
card: theme.card,
image: {
height: 260,
width: null,
},
time: {
fontSize: 14,
color: '#666',
},
metaInner: {
flexDirection: 'row',
marginVertical: 8,
},
metaIcon: {
width: 16,
paddingTop: 3,
marginHorizontal: 8,
marginVertical: 8,
},
metaText: {
color: '#000',
},
metaSubtitle: {
color: '#666',
},
content: {
borderTopWidth: StyleSheet.hairlineWidth,
borderColor: '#ccc',
marginTop: 8,
paddingTop: 10,
},
dateContainer: {
flex: 1,
justifyContent: 'center',
},
dateCard: {
margin: 0,
padding: 0,
paddingBottom: 6,
width: 62,
},
day: {
paddingTop: 5,
fontSize: 20,
color: '#000',
textAlign: 'center',
},
month: {
textAlign: 'center',
paddingHorizontal: 16,
},
year: {
fontSize: 9,
color: '#fff',
backgroundColor: '#f58220',
textAlign: 'center',
paddingRight: 20,
paddingBottom: 3,
paddingLeft: 20,
paddingTop: 3,
},
icons: {
fontSize: 16,
},
});
//FIXME: Ninja margin
// FIXME: Ninja margin
const HTMLStyles = StyleSheet.create({
p: {
fontSize: 16,
marginBottom: -30,
color: '#000',
},
ul: {
fontSize: 16,
color: '#000',
marginTop: -20,
marginBottom: -30,
},
});
p: {
fontSize: 16,
marginBottom: -30,
color: '#000',
},
ul: {
fontSize: 16,
color: '#000',
marginTop: -20,
marginBottom: -30,
},
});
+128 -106
View File
@@ -1,122 +1,144 @@
import {StyleSheet, View, Platform} from "react-native";
import {SectionList} from 'react-navigation';
import {Card, ListItem, CardItem, Body, Text as Text, Left, Right, Icon, Button, Content, Spinner} from 'native-base';
import { StyleSheet, View, Platform } from 'react-native';
import { SectionList } from 'react-navigation';
import { Card, ListItem, CardItem, Body, Text, Left, Right, Icon, Button, Content, Spinner } from 'native-base';
import React, { Component } from 'react';
import moment from "moment";
import moment from 'moment';
import 'moment/locale/nb';
import theme from "../../theme";
import theme from '../../theme';
moment.locale('nb');
export default class EventList extends Component {
_renderItem = ({item}) => (
<ListItem button onPress={() => { this._onPressItem(item); }} style={styles.listItem}>
<Body style={{flex: 5}}>
<Text style={styles.listItemTitle} numberOfLines={1}>{item.title.decoded}</Text>
<Text style={styles.listItemTime}>{this._formatTime(item.start_time)}</Text>
</Body>
<Right style={{flex: 1}}>
<Icon name="arrow-forward" />
</Right>
</ListItem>
_renderItem = ({ item }) => (
<ListItem
button
onPress={() => {
this._onPressItem(item);
}}
style={styles.listItem}
>
<Body style={{ flex: 5 }}>
<Text style={styles.listItemTitle} numberOfLines={1}>
{item.title.decoded}
</Text>
<Text style={styles.listItemTime}>{this._formatTime(item.start_time)}</Text>
</Body>
<Right style={{ flex: 1 }}>
<Icon name="arrow-forward" />
</Right>
</ListItem>
);
_renderSectionHeader = ({ section }) => {
return (
<ListItem style={styles.sectionHeader}>
<Body>
<Text style={styles.sectionTitle}>{section.title}</Text>
</Body>
</ListItem>
);
};
_renderSectionHeader = ({section}) => {
return (<ListItem style={styles.sectionHeader}>
<Body>
<Text style={styles.sectionTitle}>{section.title}</Text>
</Body>
</ListItem>);
};
_renderFooter = () => {
if(!this.props.loading) {
return null;
}
return (<View style={{paddingVertical: 20}}>
<Spinner color="#f58220"/>
</View>);
};
_onPressItem = (item) => {
this.props.showEvent(item);
};
_formatTime(time) {
return moment(time).format('llll');
_renderFooter = () => {
if (!this.props.loading) {
return null;
}
render() {
if (this.props.error) {
return (<Content>
<Card style={{margin: 4}}>
<CardItem onPress={() => { this._onPressItem(item); }}>
<Text style={styles.loadingText}>Kunne ikke hente programmet...</Text>
</CardItem>
<CardItem>
<Button onPress={this.props.handleRefresh}><Text>Prøv igjen</Text></Button>
</CardItem>
</Card>
</Content>)
}
return (
<View style={{ paddingVertical: 20 }}>
<Spinner color="#f58220" />
</View>
);
};
return (<SectionList
sections={this.props.eventsSectioned}
renderItem={this._renderItem}
renderSectionHeader={this._renderSectionHeader}
keyExtractor={(item) => item.id}
refreshing={this.props.refreshing}
onRefresh={this.props.handleRefresh}
onEndReached={this.props.handleLoadMore}
onEndReachedThreshould={10}
initialNumToRender={10}
ListFooterComponent={this._renderFooter}
style={[styles.card, styles.list]}
ListHeaderComponent={() => { return <View style={styles.listHeader}/> }}
/>);
_onPressItem = (item) => {
this.props.showEvent(item);
};
_formatTime(time) {
return moment(time).format('llll');
}
render() {
if (this.props.error) {
return (
<Content>
<Card style={{ margin: 4 }}>
<CardItem
onPress={() => {
this._onPressItem(item);
}}
>
<Text style={styles.loadingText}>Kunne ikke hente programmet...</Text>
</CardItem>
<CardItem>
<Button onPress={this.props.handleRefresh}>
<Text>Prøv igjen</Text>
</Button>
</CardItem>
</Card>
</Content>
);
}
return (
<SectionList
sections={this.props.eventsSectioned}
renderItem={this._renderItem}
renderSectionHeader={this._renderSectionHeader}
keyExtractor={(item) => item.id}
refreshing={this.props.refreshing}
onRefresh={this.props.handleRefresh}
onEndReached={this.props.handleLoadMore}
onEndReachedThreshould={10}
initialNumToRender={10}
ListFooterComponent={this._renderFooter}
style={[styles.card, styles.list]}
ListHeaderComponent={() => {
return <View style={styles.listHeader} />;
}}
/>
);
}
}
const styles = StyleSheet.create({
listItem: {
backgroundColor: 'white',
marginLeft: 0
},
listItemTitle: {
fontSize: 14,
},
listItemTime: {
fontSize: 14,
color: '#666'
},
sectionHeader: {
backgroundColor: 'white',
marginLeft: 0,
...Platform.select({
'ios': {marginBottom: -8},
}),
paddingBottom: 0,
borderBottomWidth: 0,
},
sectionTitle: {
fontSize: 16,
fontWeight: 'bold',
},
loadingText: {
textAlign: 'center',
paddingBottom: 8
},
card: theme.card,
list: {
padding: 0,
marginTop: 0,
marginBottom: 0,
backgroundColor: 'transparent',
},
listHeader: {
paddingTop: 8,
}
listItem: {
backgroundColor: 'white',
marginLeft: 0,
},
listItemTitle: {
fontSize: 14,
},
listItemTime: {
fontSize: 14,
color: '#666',
},
sectionHeader: {
backgroundColor: 'white',
marginLeft: 0,
...Platform.select({
ios: { marginBottom: -8 },
}),
paddingBottom: 0,
borderBottomWidth: 0,
},
sectionTitle: {
fontSize: 16,
fontWeight: 'bold',
},
loadingText: {
textAlign: 'center',
paddingBottom: 8,
},
card: theme.card,
list: {
padding: 0,
marginTop: 0,
marginBottom: 0,
backgroundColor: 'transparent',
},
listHeader: {
paddingTop: 8,
},
});
+108 -100
View File
@@ -1,119 +1,127 @@
import {connect} from "react-redux";
import { connect } from 'react-redux';
import React, {Component} from 'react';
import React, { Component } from 'react';
import Config from 'react-native-config';
import moment from "moment";
import moment from 'moment';
import 'moment/locale/nb';
import EventList from "./EventList";
import EventList from './EventList';
moment.locale('nb');
/* Ref: https://medium.com/react-native-development/how-to-use-the-flatlist-component-react-native-basics-92c482816fe6 */
class EventListContainer extends Component {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.state = {
loading: false,
events: [],
eventsSectioned: [],
page: 1,
totalPages: 0,
error: null,
refreshing: false,
};
}
_formatDate(time) {
const m = moment(time);
if(moment().year() === m.year() ) {
return m.format('dddd D. MMM')
}
return m.format('ll');
}
_toSectionFormat(events) {
let sectioned = {};
events.map((event) => {
const title = this._formatDate(event.start_time);
if( title in sectioned ) {
sectioned[title].push(event);
} else {
sectioned[title] = [event];
}
});
let sectionedList = [];
Object.entries(sectioned).forEach(([title, section]) => {
sectionedList.push({data: section, title: title});
});
return sectionedList;
}
handleRefresh = () => {
this.setState({
page: 1,
refreshing: true,
totalPages: 1,
}, () => {
this.fetchEvents()
});
this.state = {
loading: false,
events: [],
eventsSectioned: [],
page: 1,
totalPages: 0,
error: null,
refreshing: false,
};
}
handleLoadMore = () => {
if(this.state.page >= this.state.totalPages || this.state.loading) {
return;
}
this.setState({
page: this.state.page + 1,
}, () => {
this.fetchEvents();
})
};
fetchEvents() {
const { page } = this.state;
const url = `${Config.EVENT_API_URL}/wp-json/wp/v2/events?page=${page}&future=1`;
this.setState({ loading: true });
fetch(url)
.then(res => {
this.setState({totalPages: parseInt(res.headers.get('x-wp-totalpages'))});
return res.json();
})
.then(res => {
this.setState({
events: page === 1 ? res : [...this.state.events, ...res],
eventsSectioned: page === 1 ? this._toSectionFormat(res) : this._toSectionFormat([...this.state.events, ...res]),
error: res.error || null,
loading: false,
refreshing: false
});
})
.catch(error => {
this.setState({ error, loading: false });
});
_formatDate(time) {
const m = moment(time);
if (moment().year() === m.year()) {
return m.format('dddd D. MMM');
}
return m.format('ll');
}
componentDidMount() {
_toSectionFormat(events) {
const sectioned = {};
events.map((event) => {
const title = this._formatDate(event.start_time);
if (title in sectioned) {
sectioned[title].push(event);
} else {
sectioned[title] = [event];
}
});
const sectionedList = [];
Object.entries(sectioned).forEach(([title, section]) => {
sectionedList.push({ data: section, title });
});
return sectionedList;
}
handleRefresh = () => {
this.setState(
{
page: 1,
refreshing: true,
totalPages: 1,
},
() => {
this.fetchEvents();
}
}
);
};
showEvent = (item) => {
this.props.showEvent(item);
};
render() {
return (<EventList
showEvent={this.showEvent}
handleRefresh={this.handleRefresh}
handleLoadMore={this.handleLoadMore}
{...this.state}
/>)
handleLoadMore = () => {
if (this.state.page >= this.state.totalPages || this.state.loading) {
return;
}
this.setState(
{
page: this.state.page + 1,
},
() => {
this.fetchEvents();
}
);
};
fetchEvents() {
const { page } = this.state;
const url = `${Config.EVENT_API_URL}/wp-json/wp/v2/events?page=${page}&future=1`;
this.setState({ loading: true });
fetch(url)
.then((res) => {
this.setState({ totalPages: parseInt(res.headers.get('x-wp-totalpages')) });
return res.json();
})
.then((res) => {
this.setState({
events: page === 1 ? res : [...this.state.events, ...res],
eventsSectioned:
page === 1 ? this._toSectionFormat(res) : this._toSectionFormat([...this.state.events, ...res]),
error: res.error || null,
loading: false,
refreshing: false,
});
})
.catch((error) => {
this.setState({ error, loading: false });
});
}
componentDidMount() {
this.fetchEvents();
}
showEvent = (item) => {
this.props.showEvent(item);
};
render() {
return (
<EventList
showEvent={this.showEvent}
handleRefresh={this.handleRefresh}
handleLoadMore={this.handleLoadMore}
{...this.state}
/>
);
}
}
export default connect()(EventListContainer);
export default connect()(EventListContainer);
+157 -146
View File
@@ -1,162 +1,173 @@
import React, {Component} from 'react';
import {Linking, StyleSheet, View, Platform} from 'react-native';
import {Container, Header, Content, Form, Item, Input, Label, Spinner, Button, Text,
Icon} from 'native-base';
import React, { Component } from 'react';
import { Linking, StyleSheet, View, Platform } from 'react-native';
import { Container, Header, Content, Form, Item, Input, Label, Spinner, Button, Text, Icon } from 'native-base';
import Config from 'react-native-config';
import theme from "../../theme";
import theme from '../../theme';
export default class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: ''
}
}
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
};
}
emailInput() {
if (this.props.loginError && this.props.loginError.username) {
return <Item stackedLabel last error>
<Label>E-post / Brukernavn</Label>
<Input
keyboardType="email-address"
autoFocus={true}
autoCapitalize="none"
autoCorrect={false}
returnKeyType="next"
onChangeText={this.handleEmail}
onSubmitEditing={() => {
this.refs.passwordInput._root.focus()
}}
/>
</Item>;
}
return <Item stackedLabel last>
<Label>E-post / Brukernavn</Label>
<Input
keyboardType="email-address"
autoFocus={true}
autoCapitalize="none"
autoCorrect={false}
returnKeyType="next"
onChangeText={this.handleEmail}
onSubmitEditing={() => {
this.refs.passwordInput._root.focus()
}}
/>
</Item>;
}
passwordInput() {
if (this.props.loginError && this.props.loginError.password) {
return <Item stackedLabel last error>
<Label>Passord</Label>
<Input
ref="passwordInput"
secureTextEntry={true}
autoCapitalize="none"
autoCorrect={false}
onChangeText={this.handlePassword}
onSubmitEditing={this.onLoginPress}
/>
</Item>
}
return <Item stackedLabel last>
<Label>Passord</Label>
<Input
ref="passwordInput"
secureTextEntry={true}
autoCapitalize="none"
autoCorrect={false}
onChangeText={this.handlePassword}
onSubmitEditing={this.onLoginPress}
/>
emailInput() {
if (this.props.loginError && this.props.loginError.username) {
return (
<Item stackedLabel last error>
<Label>E-post / Brukernavn</Label>
<Input
keyboardType="email-address"
autoFocus={true}
autoCapitalize="none"
autoCorrect={false}
returnKeyType="next"
onChangeText={this.handleEmail}
onSubmitEditing={() => {
this.refs.passwordInput._root.focus();
}}
/>
</Item>
);
}
render() {
return (
<Container style={styles.container}>
<Content keyboardShouldPersistTaps='always'>
<Form style={styles.card}>
{this.emailInput()}
{this.passwordInput()}
{this.showError()}
<Button full onPress={this.onLoginPress} style={styles.loginButton}>
<Text>Logg inn</Text>
</Button>
<Button
small
light
onPress={() => { Linking.openURL(Config.DUSKEN_FORGOT_PASSWORD_URL); } }
style={styles.forgotPasswordButton}
>
<Text>Glemt passord?</Text>
</Button>
{this.showSpinner()}
</Form>
</Content>
</Container>
);
return (
<Item stackedLabel last>
<Label>E-post / Brukernavn</Label>
<Input
keyboardType="email-address"
autoFocus={true}
autoCapitalize="none"
autoCorrect={false}
returnKeyType="next"
onChangeText={this.handleEmail}
onSubmitEditing={() => {
this.refs.passwordInput._root.focus();
}}
/>
</Item>
);
}
passwordInput() {
if (this.props.loginError && this.props.loginError.password) {
return (
<Item stackedLabel last error>
<Label>Passord</Label>
<Input
ref="passwordInput"
secureTextEntry={true}
autoCapitalize="none"
autoCorrect={false}
onChangeText={this.handlePassword}
onSubmitEditing={this.onLoginPress}
/>
</Item>
);
}
return (
<Item stackedLabel last>
<Label>Passord</Label>
<Input
ref="passwordInput"
secureTextEntry={true}
autoCapitalize="none"
autoCorrect={false}
onChangeText={this.handlePassword}
onSubmitEditing={this.onLoginPress}
/>
</Item>
);
}
render() {
return (
<Container style={styles.container}>
<Content keyboardShouldPersistTaps="always">
<Form style={styles.card}>
{this.emailInput()}
{this.passwordInput()}
{this.showError()}
<Button full onPress={this.onLoginPress} style={styles.loginButton}>
<Text>Logg inn</Text>
</Button>
<Button
small
light
onPress={() => {
Linking.openURL(Config.DUSKEN_FORGOT_PASSWORD_URL);
}}
style={styles.forgotPasswordButton}
>
<Text>Glemt passord?</Text>
</Button>
{this.showSpinner()}
</Form>
</Content>
</Container>
);
}
showError = () => {
if (this.props.loginError) {
const err = this.props.loginError.non_field_errors;
const errorFormatted = err ? err[0] : 'Feil brukernavn eller passord, prøv igjen...';
// TODO: Format these and highlight error fields
return (
<View style={styles.errorBox}>
<Text style={styles.errorMessage}>{errorFormatted}</Text>
</View>
);
}
showError = () => {
if (this.props.loginError) {
const err = this.props.loginError.non_field_errors;
let errorFormatted = err ? err[0] : 'Feil brukernavn eller passord, prøv igjen...';
// TODO: Format these and highlight error fields
return <View style={styles.errorBox}><Text style={styles.errorMessage}>{errorFormatted}</Text></View>
}
else {
return <View style={styles.errorBox}/>
}
};
return <View style={styles.errorBox} />;
};
showSpinner = () => {
if (this.props.isLoggingIn) {
return <Spinner color="#f58220"/>
}
};
showSpinner = () => {
if (this.props.isLoggingIn) {
return <Spinner color="#f58220" />;
}
};
handleEmail = (text) => {
this.setState({email: text})
};
handleEmail = (text) => {
this.setState({ email: text });
};
handlePassword = (text) => {
this.setState({password: text})
};
onLoginPress = () => {
this.props.requestLogin(this.state.email, this.state.password)
};
handlePassword = (text) => {
this.setState({ password: text });
};
onLoginPress = () => {
this.props.requestLogin(this.state.email, this.state.password);
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
input: {
width: 320,
fontSize: 20,
height: 52,
},
loginButton: {
marginBottom: 16,
},
forgotPasswordButton: {
alignSelf: 'center',
marginTop: 32,
marginBottom: 16,
},
errorBox: {
marginVertical: 10,
},
errorMessage: {
textAlign: 'center',
color: 'red',
},
card: theme.card,
});
container: {
flex: 1,
justifyContent: 'center',
},
input: {
width: 320,
fontSize: 20,
height: 52,
},
loginButton: {
marginBottom: 16,
},
forgotPasswordButton: {
alignSelf: 'center',
marginTop: 32,
marginBottom: 16,
},
errorBox: {
marginVertical: 10,
},
errorMessage: {
textAlign: 'center',
color: 'red',
},
card: theme.card,
});
+19 -19
View File
@@ -1,27 +1,27 @@
import React, {Component} from 'react';
import {connect} from "react-redux";
import Login from "./Login";
import {requestLogin} from "../../api";
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Login from './Login';
import { requestLogin } from '../../api';
class LoginContainer extends Component {
componentWillReceiveProps(nextProps) {
if (this.props.isAuthenticated !== nextProps.isAuthenticated) {
if (nextProps.isAuthenticated) {
this.props.onLogin();
}
}
componentWillReceiveProps(nextProps) {
if (this.props.isAuthenticated !== nextProps.isAuthenticated) {
if (nextProps.isAuthenticated) {
this.props.onLogin();
}
}
}
render() {
return <Login {...this.props} />
}
render() {
return <Login {...this.props} />;
}
}
export default connect(
(store) => ({
isAuthenticated: store.isAuthenticated,
isLoggingIn: store.isLoggingIn,
loginError: store.loginError,
}),
{requestLogin}
(store) => ({
isAuthenticated: store.isAuthenticated,
isLoggingIn: store.isLoggingIn,
loginError: store.loginError,
}),
{ requestLogin }
)(LoginContainer);
+75 -73
View File
@@ -1,80 +1,82 @@
import React, {Component} from 'react';
import {ScrollView, Text, View, StyleSheet} from "react-native";
import {Button, Text as NBText, Spinner} from "native-base";
import theme from "../../theme";
import React, { Component } from 'react';
import { ScrollView, Text, View, StyleSheet } from 'react-native';
import { Button, Text as NBText, Spinner } from 'native-base';
import theme from '../../theme';
export default class Membership extends Component {
render() {
if (this.props.isFetchingUserData) {
return (
<View style={styles.card}>
<Spinner color="#f58220"/>
</View>
);
}
return (
<ScrollView>
<View style={styles.card}>
<Text style={styles.headerTitle}>Hei 👋</Text>
<Text style={styles.paragraph}>Her kan du bli medlem i Det Norske Studentersamfund - Chateau Neuf og ha medlemsbeviset ditt på telefonen</Text>
<View style={styles.button}>
<Button onPress={this.props.onRegisterPress} full><NBText>Bli medlem</NBText></Button>
</View>
<Text style={styles.header}>Hva får jeg?</Text>
<Text style={[styles.paragraph, {textAlign: 'left', paddingLeft: 4}]}>
- Minst 25% rabatt på de fleste arrangement{"\n"}
- Gratis inngang på enkelte arrangement{"\n"}
- Store rabatter i barene{"\n"}
- Egne medlemsarrangement og -tilbud{"\n"}
- Mulighet til å delta i landets eldste studentdemokrati</Text>
<Text style={styles.em}>Allerede medlem?</Text>
<View style={styles.button}>
<Button onPress={this.props.onLoginPress} full><NBText>Logg inn</NBText></Button>
</View>
</View>
</ScrollView>
)
};
render() {
if (this.props.isFetchingUserData) {
return (
<View style={styles.card}>
<Spinner color="#f58220" />
</View>
);
}
return (
<ScrollView>
<View style={styles.card}>
<Text style={styles.headerTitle}>Hei 👋</Text>
<Text style={styles.paragraph}>
Her kan du bli medlem i Det Norske Studentersamfund - Chateau Neuf og ha medlemsbeviset ditt på telefonen
</Text>
<View style={styles.button}>
<Button onPress={this.props.onRegisterPress} full>
<NBText>Bli medlem</NBText>
</Button>
</View>
<Text style={styles.header}>Hva får jeg?</Text>
<Text style={[styles.paragraph, { textAlign: 'left', paddingLeft: 4 }]}>
- Minst 25% rabatt på de fleste arrangement{'\n'}- Gratis inngang på enkelte arrangement{'\n'}- Store
rabatter i barene{'\n'}- Egne medlemsarrangement og -tilbud{'\n'}- Mulighet til å delta i landets eldste
studentdemokrati
</Text>
<Text style={styles.em}>Allerede medlem?</Text>
<View style={styles.button}>
<Button onPress={this.props.onLoginPress} full>
<NBText>Logg inn</NBText>
</Button>
</View>
</View>
</ScrollView>
);
}
}
const styles = StyleSheet.create({
card: theme.card,
paragraph: {
textAlign: 'center',
fontSize: 16,
color: '#000',
paddingBottom: 10,
lineHeight: 25,
},
button: {
marginTop: 6,
marginBottom: 16,
},
em: {
fontSize: 16,
color: '#000',
fontStyle: 'italic',
textAlign: 'center',
marginVertical: 10,
},
headerTitle: {
textAlign: 'center',
fontWeight: 'bold',
fontSize: 20,
color: '#000',
paddingVertical: 10,
lineHeight: 25,
},
header: {
fontSize: 16,
fontWeight: 'bold',
color: '#000',
lineHeight: 25,
paddingLeft: 2
},
});
card: theme.card,
paragraph: {
textAlign: 'center',
fontSize: 16,
color: '#000',
paddingBottom: 10,
lineHeight: 25,
},
button: {
marginTop: 6,
marginBottom: 16,
},
em: {
fontSize: 16,
color: '#000',
fontStyle: 'italic',
textAlign: 'center',
marginVertical: 10,
},
headerTitle: {
textAlign: 'center',
fontWeight: 'bold',
fontSize: 20,
color: '#000',
paddingVertical: 10,
lineHeight: 25,
},
header: {
fontSize: 16,
fontWeight: 'bold',
color: '#000',
lineHeight: 25,
paddingLeft: 2,
},
});
+68 -65
View File
@@ -1,86 +1,89 @@
import React, {Component} from 'react';
import {connect} from "react-redux";
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Membership from "./Membership";
import ProofContainer from "./ProofContainer";
import { logout } from "../../actions";
import {requestUserData} from "../../api";
import Membership from './Membership';
import ProofContainer from './ProofContainer';
import { logout } from '../../actions';
import { requestUserData } from '../../api';
class MembershipContainer extends Component {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.state = {
isFetchingUserData: props.isFetchingUserData,
userToken: props.userToken,
lastOrder: props.lastOrder,
this.state = {
isFetchingUserData: props.isFetchingUserData,
userToken: props.userToken,
lastOrder: props.lastOrder,
};
}
componentWillReceiveProps(nextProps) {
if (this.state.userToken !== nextProps.userToken) {
this.setState({ userToken: nextProps.userToken }, () => {
if (nextProps.userToken) {
this.fetchUser();
}
});
}
componentWillReceiveProps(nextProps) {
if(this.state.userToken !== nextProps.userToken) {
this.setState({userToken: nextProps.userToken}, () => {
if(nextProps.userToken) {
this.fetchUser();
}
});
}
if(this.state.lastOrder !== nextProps.lastOrder) {
this.setState({lastOrder: nextProps.lastOrder}, () => {
if(nextProps.userToken) {
this.fetchUser();
}
});
if (this.state.lastOrder !== nextProps.lastOrder) {
this.setState({ lastOrder: nextProps.lastOrder }, () => {
if (nextProps.userToken) {
this.fetchUser();
}
});
}
}
fetchUser = () => {
this.props.requestUserData(this.state.userToken)
};
fetchUser = () => {
this.props.requestUserData(this.state.userToken);
};
onLoginPress = () => {
this.props.onLoginPress();
};
onLoginPress = () => {
this.props.onLoginPress();
};
onRegisterPress = () => {
this.props.onRegisterPress();
};
onRegisterPress = () => {
this.props.onRegisterPress();
};
onLogoutPress = () => {
this.props.logout();
this.props.onLogoutPress();
};
onLogoutPress = () => {
this.props.logout();
this.props.onLogoutPress();
};
render() {
if (this.props.user) {
return <ProofContainer
user={this.props.user}
isAuthenticated={this.props.isAuthenticated}
userToken={this.state.userToken}
fetchUser={this.fetchUser}
onLogoutPress={this.onLogoutPress}
/>;
}
return <Membership
isFetchingUserData={this.props.isFetchingUserData}
onLoginPress={this.onLoginPress}
onRegisterPress={this.onRegisterPress}
/>;
render() {
if (this.props.user) {
return (
<ProofContainer
user={this.props.user}
isAuthenticated={this.props.isAuthenticated}
userToken={this.state.userToken}
fetchUser={this.fetchUser}
onLogoutPress={this.onLogoutPress}
/>
);
}
return (
<Membership
isFetchingUserData={this.props.isFetchingUserData}
onLoginPress={this.onLoginPress}
onRegisterPress={this.onRegisterPress}
/>
);
}
}
function storeToProps(store) {
return {
isAuthenticated: store.isAuthenticated,
isFetchingUserData: store.isFetchingUserData,
userToken: store.userToken,
user: store.user,
lastOrder: store.lastOrder
};
return {
isAuthenticated: store.isAuthenticated,
isFetchingUserData: store.isFetchingUserData,
userToken: store.userToken,
user: store.user,
lastOrder: store.lastOrder,
};
}
export default connect(
storeToProps,
{ requestUserData, logout }
storeToProps,
{ requestUserData, logout }
)(MembershipContainer);
+236 -223
View File
@@ -1,241 +1,254 @@
import React, {Component} from 'react';
import {Image, ScrollView, StyleSheet, Text, TouchableOpacity, View} from "react-native";
import {Button, Text as NBText, Spinner} from 'native-base';
import React, { Component } from 'react';
import { Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Button, Text as NBText, Spinner } from 'native-base';
import Confetti from 'react-native-confetti';
import theme from "../../theme";
import theme from '../../theme';
export default class Proof extends Component {
CONFETTI_TIMEOUT = 15000;
CONFETTI_TIMEOUT = 15000;
constructor(props) {
super(props);
this.state = {
stopConfetti: false,
}
constructor(props) {
super(props);
this.state = {
stopConfetti: false,
};
}
componentDidUpdate () {
if (this._confettiView && this.state.stopConfetti) {
this._confettiView.stopConfetti();
this.setState({stopConfetti: false});
}
componentDidUpdate() {
if (this._confettiView && this.state.stopConfetti) {
this._confettiView.stopConfetti();
this.setState({ stopConfetti: false });
}
}
componentWillUnmount() {
if (this._confettiView) {
this._confettiView.stopConfetti();
}
}
onMembershipPress = () => {
this.setState({ showConfetti: true }, () => {
if (this._confettiView) {
this._confettiView.startConfetti();
setTimeout(
function() {
this.setState({ stopConfetti: true });
}.bind(this),
this.CONFETTI_TIMEOUT
);
}
});
};
membershipValidTo() {
if (!this.props.user.last_membership) {
return;
}
componentWillUnmount () {
if (this._confettiView) {
this._confettiView.stopConfetti();
}
let validTo = this.props.user.last_membership.end_date;
if (this.props.user.last_membership.membership_type === 'lifelong') {
validTo = 'Livsvarig';
}
onMembershipPress = () => {
this.setState({showConfetti: true}, () => {
if(this._confettiView) {
this._confettiView.startConfetti();
setTimeout(function() {
this.setState({stopConfetti: true});
}.bind(this), this.CONFETTI_TIMEOUT);
}
})
};
return (
<View style={{ alignItems: 'center', flex: 1 }}>
<Text>Gyldig til: </Text>
<Text style={styles.validToValue}>{validTo}</Text>
</View>
);
}
membershipValidTo() {
if( !this.props.user.last_membership) {
return;
}
membershipStatus() {
if (!this.props.user.is_member) {
if (!this.props.user.last_membership) {
return;
}
let validTo = this.props.user.last_membership.end_date;
if (this.props.user.last_membership.membership_type === 'lifelong') {
validTo = 'Livsvarig'
}
return (
<View style={{alignItems: 'center', flex: 1}}>
<Text>Gyldig til: </Text>
<Text style={styles.validToValue}>{validTo}</Text>
</View>
)
}
membershipStatus() {
if (!this.props.user.is_member) {
if ( !this.props.user.last_membership ) {
return;
}
return (
<View style={styles.expired}>
<Text style={styles.expiredText}>Utløpt</Text>
</View>)
}
const statusText = this.props.user.is_volunteer ? '❤️ AKTIV ❤️ ' : 'MEDLEM';
return (
<View style={styles.membershipStatus}>
<TouchableOpacity onPress={this.onMembershipPress} style={styles.valid}>
<Text style={styles.validText}>{statusText}</Text>
</TouchableOpacity>
</View>);
}
purchaseButton() {
if (this.props.user.is_member) {
return;
}
return (
<View>
{this.props.isChargingMembership && <Spinner color="#f58220"/>}
<Button
onPress={this.props.onChargePress}
full
disabled={this.props.isChargingMembership || this.props.isLoadingMembershipType}
style={styles.purchaseButton}
>
<NBText>{`Kjøp medlemskap (${this.props.membershipPrice} NOK)`}</NBText>
</Button>
<View style={styles.purchaseText}>
<Text style={styles.purchaseTextInner}>Medlemskapet er gyldig i ett år</Text>
</View>
</View>)
}
confetti() {
if (this.props.user.is_member) {
// Confetti!!!!
return <Confetti ref={(node) => this._confettiView = node} confettiCount={Number.MAX_SAFE_INTEGER} />
}
}
logo() {
if (!this.props.user.is_member) {
return;
}
// FIXME: URL to config
return (
<View style={{marginTop: 8}}>
<Image
style={{width: null, height: 100}}
source={{uri: 'https://galtinn.neuf.no/static/dist/images/logo.png'}}
resizeMode="contain"
/>
</View>)
}
chargeError() {
if (!this.props.chargeError) {
return;
}
return <View><Text style={styles.errorMessage}>{this.props.chargeError}</Text></View>
}
render() {
return (
<View>
{this.confetti()}
<ScrollView>
<View style={[styles.card, {paddingVertical: 16}]}>
{this.membershipName()}
{this.logo()}
{this.membershipStatus()}
{this.chargeError()}
{this.purchaseButton()}
{this.membershipValidTo()}
</View>
<View>
<Button onPress={this.props.onLogoutPress} style={styles.logoutButton} small>
<NBText>Logg ut</NBText>
</Button>
</View>
</ScrollView>
</View>
)
};
membershipName() {
let name = `${this.props.user.first_name} ${this.props.user.last_name}`;
if(name === ' ') {
name = this.props.user.email;
}
return <View>
<Text style={styles.nameText}>{name}</Text>
return (
<View style={styles.expired}>
<Text style={styles.expiredText}>Utløpt</Text>
</View>
);
}
const statusText = this.props.user.is_volunteer ? '❤️ AKTIV ❤️ ' : 'MEDLEM';
return (
<View style={styles.membershipStatus}>
<TouchableOpacity onPress={this.onMembershipPress} style={styles.valid}>
<Text style={styles.validText}>{statusText}</Text>
</TouchableOpacity>
</View>
);
}
purchaseButton() {
if (this.props.user.is_member) {
return;
}
return (
<View>
{this.props.isChargingMembership && <Spinner color="#f58220" />}
<Button
onPress={this.props.onChargePress}
full
disabled={this.props.isChargingMembership || this.props.isLoadingMembershipType}
style={styles.purchaseButton}
>
<NBText>{`Kjøp medlemskap (${this.props.membershipPrice} NOK)`}</NBText>
</Button>
<View style={styles.purchaseText}>
<Text style={styles.purchaseTextInner}>Medlemskapet er gyldig i ett år</Text>
</View>
</View>
);
}
confetti() {
if (this.props.user.is_member) {
// Confetti!!!!
return <Confetti ref={(node) => (this._confettiView = node)} confettiCount={Number.MAX_SAFE_INTEGER} />;
}
}
logo() {
if (!this.props.user.is_member) {
return;
}
// FIXME: URL to config
return (
<View style={{ marginTop: 8 }}>
<Image
style={{ width: null, height: 100 }}
source={{ uri: 'https://galtinn.neuf.no/static/dist/images/logo.png' }}
resizeMode="contain"
/>
</View>
);
}
chargeError() {
if (!this.props.chargeError) {
return;
}
return (
<View>
<Text style={styles.errorMessage}>{this.props.chargeError}</Text>
</View>
);
}
render() {
return (
<View>
{this.confetti()}
<ScrollView>
<View style={[styles.card, { paddingVertical: 16 }]}>
{this.membershipName()}
{this.logo()}
{this.membershipStatus()}
{this.chargeError()}
{this.purchaseButton()}
{this.membershipValidTo()}
</View>
<View>
<Button onPress={this.props.onLogoutPress} style={styles.logoutButton} small>
<NBText>Logg ut</NBText>
</Button>
</View>
</ScrollView>
</View>
);
}
membershipName() {
let name = `${this.props.user.first_name} ${this.props.user.last_name}`;
if (name === ' ') {
name = this.props.user.email;
}
return (
<View>
<Text style={styles.nameText}>{name}</Text>
</View>
);
}
}
const styles = StyleSheet.create({
card: theme.card,
membershipStatus: {
marginVertical: 16,
},
nameText: {
flex: 1,
textAlign: 'center',
fontSize: 20,
fontWeight: 'bold',
},
valid: {
backgroundColor: '#5cb85c',
flex: 1,
marginHorizontal: -16,
padding: 16
},
validText: {
color: 'white',
textAlign: 'center',
fontSize: 30,
fontWeight: "800"
},
expired: {
backgroundColor: '#d9534f',
flex: 1,
marginHorizontal: -16,
padding: 16,
marginVertical: 16,
},
expiredText: {
color: 'white',
textAlign: 'center',
fontSize: 24
},
notMember: {
backgroundColor: '#62B1F6',
flex: 1,
marginHorizontal: -16,
padding: 16
},
notMemberText: {
color: 'white',
textAlign: 'center',
fontSize: 24
},
validToValue: {
fontWeight: 'bold',
fontSize: 18,
},
purchaseButton: {
marginTop: 16,
},
logoutButton: {
alignSelf: 'center',
marginVertical: 20,
},
purchaseText: {
alignItems: 'center'
},
purchaseTextInner: {
fontStyle: 'italic',
fontSize: 16,
textAlign: 'center',
paddingTop: 10,
paddingBottom: 8
},
errorMessage: {
paddingTop: 10,
textAlign: 'center',
color: theme.colors.danger
}
});
card: theme.card,
membershipStatus: {
marginVertical: 16,
},
nameText: {
flex: 1,
textAlign: 'center',
fontSize: 20,
fontWeight: 'bold',
},
valid: {
backgroundColor: '#5cb85c',
flex: 1,
marginHorizontal: -16,
padding: 16,
},
validText: {
color: 'white',
textAlign: 'center',
fontSize: 30,
fontWeight: '800',
},
expired: {
backgroundColor: '#d9534f',
flex: 1,
marginHorizontal: -16,
padding: 16,
marginVertical: 16,
},
expiredText: {
color: 'white',
textAlign: 'center',
fontSize: 24,
},
notMember: {
backgroundColor: '#62B1F6',
flex: 1,
marginHorizontal: -16,
padding: 16,
},
notMemberText: {
color: 'white',
textAlign: 'center',
fontSize: 24,
},
validToValue: {
fontWeight: 'bold',
fontSize: 18,
},
purchaseButton: {
marginTop: 16,
},
logoutButton: {
alignSelf: 'center',
marginVertical: 20,
},
purchaseText: {
alignItems: 'center',
},
purchaseTextInner: {
fontStyle: 'italic',
fontSize: 16,
textAlign: 'center',
paddingTop: 10,
paddingBottom: 8,
},
errorMessage: {
paddingTop: 10,
textAlign: 'center',
color: theme.colors.danger,
},
});
+65 -80
View File
@@ -1,105 +1,90 @@
import React, {Component} from 'react';
import {connect} from "react-redux";
import stripe from 'tipsi-stripe'
import React, { Component } from 'react';
import { connect } from 'react-redux';
import stripe from 'tipsi-stripe';
import Config from 'react-native-config';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import Proof from "./Proof";
import {requestMembershipCharge} from "../../api";
import Proof from './Proof';
import { requestMembershipCharge } from '../../api';
stripe.setOptions({
publishableKey: Config.STRIPE_PUBLISHABLE_KEY || '',
publishableKey: Config.STRIPE_PUBLISHABLE_KEY || '',
});
class ProofContainer extends Component {
constructor(props) {
super(props);
this.state = {
refreshing: false,
}
constructor(props) {
super(props);
this.state = {
refreshing: false,
};
}
getPrice(membershipTypes) {
if (!membershipTypes) {
return 0;
}
getPrice(membershipTypes) {
if (!membershipTypes) {
return 0;
}
return membershipTypes[0].price / 100;
}
return membershipTypes[0].price / 100;
}
render() {
return (
<Proof
{...this.state}
user={this.props.user}
isChargingMembership={this.props.isChargingMembership}
isLoadingMembershipType={this.props.data.loading}
chargeError={this.props.chargeError ? this.props.chargeError : ''}
onChargePress={this.openStripe}
onLogoutPress={this.props.onLogoutPress}
membershipPrice={this.getPrice(this.props.data.membershipTypes)}
/>
);
}
render() {
return <Proof
{...this.state}
user={this.props.user}
isChargingMembership={this.props.isChargingMembership}
isLoadingMembershipType={this.props.data.loading}
chargeError={this.props.chargeError ? this.props.chargeError : ''}
onChargePress={this.openStripe}
onLogoutPress={this.props.onLogoutPress}
membershipPrice={this.getPrice(this.props.data.membershipTypes)}
/>
}
openStripe = () => {
const options = {
prefilledInformation: {
email: this.props.user.email,
},
smsAutofillDisabled: true,
};
const membershipTypeSlug = this.props.data.membershipTypes ? this.props.data.membershipTypes[0].slug : 'standard';
stripe.paymentRequestWithCardForm(options).then(
(token) => {
// returns a token.tokenId which should be saved
this.props.requestMembershipCharge(
this.props.userToken,
{id: token.tokenId, email: this.props.user.email},
membershipTypeSlug);
},
(error) => { /* FIXME: Happens only if card dialog is canceled? */ }
);
openStripe = () => {
const options = {
prefilledInformation: {
email: this.props.user.email,
},
smsAutofillDisabled: true,
};
// fetchUser = () => {
// this.props.fetchUser().then(() => {
// this.setState({refreshing: false});
// });
// };
const membershipTypeSlug = this.props.data.membershipTypes ? this.props.data.membershipTypes[0].slug : 'standard';
// _onRefresh() {
// console.log('onRefresh')
// this.setState({refreshing: true});
// this.fetchUser()
// }
// refreshControl() {
// return (
// <RefreshControl
// refreshing={this.state.refreshing}
// onRefresh={this._onRefresh.bind(this)}
// />)
// }
stripe.paymentRequestWithCardForm(options).then(
(token) => {
// returns a token.tokenId which should be saved
this.props.requestMembershipCharge(
this.props.userToken,
{ id: token.tokenId, email: this.props.user.email },
membershipTypeSlug
);
},
(error) => {
/* FIXME: Happens only if card dialog is canceled? */
}
);
};
}
const PROOF_QUERY = gql`{
const PROOF_QUERY = gql`
{
membershipTypes(isDefault: true) {
slug
price
slug
price
}
}`;
}
`;
const proofContainerWithState = connect(
(store) => ({
userToken: store.userToken,
isChargingMembership: store.isChargingMembership,
chargeError: store.chargeError
}),
{requestMembershipCharge}
(store) => ({
userToken: store.userToken,
isChargingMembership: store.isChargingMembership,
chargeError: store.chargeError,
}),
{ requestMembershipCharge }
)(ProofContainer);
export default graphql(PROOF_QUERY)(proofContainerWithState);
+271 -242
View File
@@ -1,264 +1,293 @@
import React, {Component} from 'react';
import {Container, Header, Content, Form, Item, Input, Label, Spinner, Button, Text, Icon, Toast} from 'native-base';
import {StyleSheet, View} from "react-native";
import React, { Component } from 'react';
import { Container, Header, Content, Form, Item, Input, Label, Spinner, Button, Text, Icon, Toast } from 'native-base';
import { StyleSheet, View } from 'react-native';
import EmailValidator from 'email-validator';
import theme from "../../theme";
import theme from '../../theme';
export default class UserRegister extends Component {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.state = {
firstName: '',
lastName: '',
email: '',
phoneNumber: '',
password: '',
errors: {},
touched: new Set()
}
this.state = {
firstName: '',
lastName: '',
email: '',
phoneNumber: '',
password: '',
errors: {},
touched: new Set(),
};
}
validateForm(triggeredBy) {
const fieldNames = ['firstName', 'lastName', 'email', 'phoneNumber', 'password'];
validateForm(triggeredBy) {
const fieldNames = ['firstName', 'lastName', 'email', 'phoneNumber', 'password'];
let errors = Object.assign({}, this.state.errors);
let touched = new Set(this.state.touched).add(triggeredBy);
const errors = Object.assign({}, this.state.errors);
const touched = new Set(this.state.touched).add(triggeredBy);
/* No empty fields */
for(let key of fieldNames) {
const value = this.state[key];
if(value === '') {
errors[key] = 'kan ikke være tomt';
} else if(key === 'email' && !EmailValidator.validate(value)) {
errors[key] = 'er ikke en gyldig e-post';
} else {
delete errors[key];
}
}
this.setState({errors: errors, touched});
};
fieldHasError(field) {
return this.state.touched.has(field) && field in this.state.errors;
/* No empty fields */
for (const key of fieldNames) {
const value = this.state[key];
if (value === '') {
errors[key] = 'kan ikke være tomt';
} else if (key === 'email' && !EmailValidator.validate(value)) {
errors[key] = 'er ikke en gyldig e-post';
} else {
delete errors[key];
}
}
canSubmitForm() {
return Object.keys(this.state.errors).length === 0;
}
this.setState({ errors, touched });
}
firstNameInput() {
return <Item error={this.fieldHasError('firstName')}>
<Label>Fornavn</Label>
<Input
autoFocus={true}
returnKeyType="next"
onChangeText={(firstName) => {
this.setState({firstName}, () => { this.validateForm('firstName'); });
}}
onSubmitEditing={() => {
this.refs.lastNameInput._root.focus()
}}
value={this.state.firstName}
/>
{this.fieldHasError('firstName') && <Icon name='close-circle' />}
</Item>;
}
fieldHasError(field) {
return this.state.touched.has(field) && field in this.state.errors;
}
lastNameInput() {
return <Item error={this.fieldHasError('lastName')}>
<Label>Etternavn</Label>
<Input
ref="lastNameInput"
returnKeyType="next"
onChangeText={(lastName) => {
this.setState({lastName}, () => { this.validateForm('lastName'); });
}}
onSubmitEditing={() => {
this.refs.emailInput._root.focus()
}}
value={this.state.lastName}
/>
{this.fieldHasError('lastName') && <Icon name='close-circle' />}
</Item>;
}
canSubmitForm() {
return Object.keys(this.state.errors).length === 0;
}
emailInput() {
return <Item error={this.fieldHasError('email')}>
<Label>E-post</Label>
<Input
ref="emailInput"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
returnKeyType="next"
onChangeText={(email) => {
this.setState({email}, () => { this.validateForm('email'); });
}}
onSubmitEditing={() => {
this.refs.phoneInput._root.focus()
}}
value={this.state.email}
/>
{this.fieldHasError('email') && <Icon name='close-circle' />}
</Item>;
}
phoneNumberInput() {
return <Item error={this.fieldHasError('phoneNumber')}>
<Label>Mobilnummer</Label>
<Input
ref="phoneInput"
returnKeyType="next"
keyboardType="phone-pad"
onChangeText={(phoneNumber) => {
this.setState({phoneNumber}, () => { this.validateForm('phoneNumber'); });
}}
onSubmitEditing={() => {
this.refs.passwordInput._root.focus()
}}
value={this.state.phoneNumber}
/>
{this.fieldHasError('phoneNumber') && <Icon name='close-circle' />}
</Item>;
}
passwordInput() {
return <Item error={this.fieldHasError('password')}>
<Label>Passord</Label>
<Input
ref="passwordInput"
secureTextEntry={true}
autoCapitalize="none"
autoCorrect={false}
onChangeText={(password) => {
this.setState({password}, () => { this.validateForm('password'); });
}}
onSubmitEditing={this.onRegisterPress}
value={this.state.password}
/>
{this.fieldHasError('password') && <Icon name='close-circle' />}
</Item>
}
componentWillReceiveProps(nextProps) {
if(this.props.registerError !== nextProps.registerError) {
this.setState({'errors': nextProps.registerError})
}
}
showNonFieldError = () => {
const errors = this.props.registerError;
if (Object.keys(errors).length !== 0 && 'non_field_errors' in errors) {
const err = errors.non_field_errors;
let errorFormatted = err ? err[0] : 'Kunne ikke registrere bruker, prøv igjen...';
return <View style={styles.errorBox}><Text style={styles.errorMessage}>{errorFormatted}</Text></View>
} else if (Object.keys(errors).length !== 0 && 'detail' in errors) {
const err = errors.detail;
let errorFormatted = err ? err : 'Kunne ikke registrere bruker, prøv igjen...';
return <View style={styles.errorBox}><Text style={styles.errorMessage}>{errorFormatted}</Text></View>
}
return <View style={styles.errorBox}/>
};
showFieldError = (field) => {
if(!this.fieldHasError(field) ) {
return;
}
return <Item style={styles.errorText}><Text style={styles.errorMessage}>{this.state.errors[field]}</Text></Item>;
};
showSpinner = () => {
if (this.props.isRegisteringUser) {
return <Spinner color="#f58220"/>
}
};
render() {
return (
<Container style={styles.container}>
<Content keyboardShouldPersistTaps='always'>
<Form style={styles.card}>
{this.firstNameInput()}
{this.showFieldError('firstName')}
{this.lastNameInput()}
{this.showFieldError('lastName')}
{this.emailInput()}
{this.showFieldError('email')}
{this.phoneNumberInput()}
{this.showFieldError('phoneNumber')}
{this.passwordInput()}
{this.showFieldError('password')}
{this.showNonFieldError()}
{this.registerbutton()}
{this.showSpinner()}
</Form>
</Content>
</Container>
);
}
onRegisterPress = () => {
if( !this.canSubmitForm() ) {
Toast.show({
text: 'Noen av feltene er ikke fylt ut riktig',
position: 'bottom',
buttonText: 'OK',
duration: 1500,
firstNameInput() {
return (
<Item error={this.fieldHasError('firstName')}>
<Label>Fornavn</Label>
<Input
autoFocus={true}
returnKeyType="next"
onChangeText={(firstName) => {
this.setState({ firstName }, () => {
this.validateForm('firstName');
});
return;
}
}}
onSubmitEditing={() => {
this.refs.lastNameInput._root.focus();
}}
value={this.state.firstName}
/>
{this.fieldHasError('firstName') && <Icon name="close-circle" />}
</Item>
);
}
this.props.onRegisterPress(
this.state.firstName,
this.state.lastName,
this.state.email,
this.state.phoneNumber,
this.state.password,
);
};
lastNameInput() {
return (
<Item error={this.fieldHasError('lastName')}>
<Label>Etternavn</Label>
<Input
ref="lastNameInput"
returnKeyType="next"
onChangeText={(lastName) => {
this.setState({ lastName }, () => {
this.validateForm('lastName');
});
}}
onSubmitEditing={() => {
this.refs.emailInput._root.focus();
}}
value={this.state.lastName}
/>
{this.fieldHasError('lastName') && <Icon name="close-circle" />}
</Item>
);
}
registerbutton() {
const isDisabled = this.state.touched.size === 0 || this.props.isRegisteringUser || !this.canSubmitForm();
emailInput() {
return (
<Item error={this.fieldHasError('email')}>
<Label>E-post</Label>
<Input
ref="emailInput"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
returnKeyType="next"
onChangeText={(email) => {
this.setState({ email }, () => {
this.validateForm('email');
});
}}
onSubmitEditing={() => {
this.refs.phoneInput._root.focus();
}}
value={this.state.email}
/>
{this.fieldHasError('email') && <Icon name="close-circle" />}
</Item>
);
}
return (<Button
full
disabled={isDisabled}
onPress={this.onRegisterPress}
style={styles.registerButton}
>
<Text>Registrer meg</Text>
</Button>);
phoneNumberInput() {
return (
<Item error={this.fieldHasError('phoneNumber')}>
<Label>Mobilnummer</Label>
<Input
ref="phoneInput"
returnKeyType="next"
keyboardType="phone-pad"
onChangeText={(phoneNumber) => {
this.setState({ phoneNumber }, () => {
this.validateForm('phoneNumber');
});
}}
onSubmitEditing={() => {
this.refs.passwordInput._root.focus();
}}
value={this.state.phoneNumber}
/>
{this.fieldHasError('phoneNumber') && <Icon name="close-circle" />}
</Item>
);
}
passwordInput() {
return (
<Item error={this.fieldHasError('password')}>
<Label>Passord</Label>
<Input
ref="passwordInput"
secureTextEntry={true}
autoCapitalize="none"
autoCorrect={false}
onChangeText={(password) => {
this.setState({ password }, () => {
this.validateForm('password');
});
}}
onSubmitEditing={this.onRegisterPress}
value={this.state.password}
/>
{this.fieldHasError('password') && <Icon name="close-circle" />}
</Item>
);
}
componentWillReceiveProps(nextProps) {
if (this.props.registerError !== nextProps.registerError) {
this.setState({ errors: nextProps.registerError });
}
}
showNonFieldError = () => {
const errors = this.props.registerError;
if (Object.keys(errors).length !== 0 && 'non_field_errors' in errors) {
const err = errors.non_field_errors;
const errorFormatted = err ? err[0] : 'Kunne ikke registrere bruker, prøv igjen...';
return (
<View style={styles.errorBox}>
<Text style={styles.errorMessage}>{errorFormatted}</Text>
</View>
);
}
if (Object.keys(errors).length !== 0 && 'detail' in errors) {
const err = errors.detail;
const errorFormatted = err || 'Kunne ikke registrere bruker, prøv igjen...';
return (
<View style={styles.errorBox}>
<Text style={styles.errorMessage}>{errorFormatted}</Text>
</View>
);
}
return <View style={styles.errorBox} />;
};
showFieldError = (field) => {
if (!this.fieldHasError(field)) {
return;
}
return (
<Item style={styles.errorText}>
<Text style={styles.errorMessage}>{this.state.errors[field]}</Text>
</Item>
);
};
showSpinner = () => {
if (this.props.isRegisteringUser) {
return <Spinner color="#f58220" />;
}
};
render() {
return (
<Container style={styles.container}>
<Content keyboardShouldPersistTaps="always">
<Form style={styles.card}>
{this.firstNameInput()}
{this.showFieldError('firstName')}
{this.lastNameInput()}
{this.showFieldError('lastName')}
{this.emailInput()}
{this.showFieldError('email')}
{this.phoneNumberInput()}
{this.showFieldError('phoneNumber')}
{this.passwordInput()}
{this.showFieldError('password')}
{this.showNonFieldError()}
{this.registerbutton()}
{this.showSpinner()}
</Form>
</Content>
</Container>
);
}
onRegisterPress = () => {
if (!this.canSubmitForm()) {
Toast.show({
text: 'Noen av feltene er ikke fylt ut riktig',
position: 'bottom',
buttonText: 'OK',
duration: 1500,
});
return;
}
this.props.onRegisterPress(
this.state.firstName,
this.state.lastName,
this.state.email,
this.state.phoneNumber,
this.state.password
);
};
registerbutton() {
const isDisabled = this.state.touched.size === 0 || this.props.isRegisteringUser || !this.canSubmitForm();
return (
<Button full disabled={isDisabled} onPress={this.onRegisterPress} style={styles.registerButton}>
<Text>Registrer meg</Text>
</Button>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
input: {
width: 320,
fontSize: 20,
height: 52,
},
registerButton: {
marginBottom: 16,
},
errorBox: {
marginVertical: 10,
},
errorMessage: {
textAlign: 'center',
color: theme.colors.danger,
fontSize: 14,
},
card: theme.card,
errorText: {
paddingTop: 2,
paddingBottom: 12,
}
container: {
flex: 1,
justifyContent: 'center',
},
input: {
width: 320,
fontSize: 20,
height: 52,
},
registerButton: {
marginBottom: 16,
},
errorBox: {
marginVertical: 10,
},
errorMessage: {
textAlign: 'center',
color: theme.colors.danger,
fontSize: 14,
},
card: theme.card,
errorText: {
paddingTop: 2,
paddingBottom: 12,
},
});
+38 -37
View File
@@ -1,49 +1,50 @@
import React, {Component} from 'react';
import {connect} from "react-redux";
import React, { Component } from 'react';
import { connect } from 'react-redux';
import UserRegister from "./UserRegister";
import {requestRegisterUser} from "../../api";
import UserRegister from './UserRegister';
import { requestRegisterUser } from '../../api';
class UserRegisterContainer extends Component {
constructor(state) {
super(state);
this.state = {
isAuthenticated: false,
}
}
componentWillReceiveProps(nextProps) {
if (this.state.isAuthenticated !== nextProps.isAuthenticated) {
if (nextProps.isAuthenticated) {
this.props.onRegister();
}
}
}
onRegisterPress = (firstName, lastName, email, phoneNumber, password) => {
this.props.requestRegisterUser(firstName, lastName, email, phoneNumber, password);
constructor(state) {
super(state);
this.state = {
isAuthenticated: false,
};
}
render() {
return <UserRegister
{...this.state}
onRegisterPress={this.onRegisterPress}
registerError={this.props.registerError}
isRegisteringUser={this.props.isRegisteringUser}
/>;
componentWillReceiveProps(nextProps) {
if (this.state.isAuthenticated !== nextProps.isAuthenticated) {
if (nextProps.isAuthenticated) {
this.props.onRegister();
}
}
}
onRegisterPress = (firstName, lastName, email, phoneNumber, password) => {
this.props.requestRegisterUser(firstName, lastName, email, phoneNumber, password);
};
render() {
return (
<UserRegister
{...this.state}
onRegisterPress={this.onRegisterPress}
registerError={this.props.registerError}
isRegisteringUser={this.props.isRegisteringUser}
/>
);
}
}
function storeToProps(store) {
const errors = store.registerError === null ? {} : store.registerError;
return {
isAuthenticated: store.isAuthenticated,
isRegisteringUser: store.isRegisteringUser,
registerError: errors
};
const errors = store.registerError === null ? {} : store.registerError;
return {
isAuthenticated: store.isAuthenticated,
isRegisteringUser: store.isRegisteringUser,
registerError: errors,
};
}
export default connect(
storeToProps,
{requestRegisterUser}
storeToProps,
{ requestRegisterUser }
)(UserRegisterContainer);
+55 -50
View File
@@ -1,67 +1,72 @@
import {Platform, StyleSheet} from 'react-native';
import {createBottomTabNavigator, createStackNavigator, createAppContainer, getActiveChildNavigationOptions} from "react-navigation";
import { Platform, StyleSheet } from 'react-native';
import {
createBottomTabNavigator,
createStackNavigator,
createAppContainer,
getActiveChildNavigationOptions,
} from 'react-navigation';
import AboutScreen from "./screens/AboutScreen";
import EventDetailScreen from "./screens/EventDetailScreen";
import EventListScreen from "./screens/EventListScreen";
import LoginScreen from "./screens/LoginScreen";
import MembershipScreen from "./screens/MembershipScreen";
import UserRegisterScreen from "./screens/UserRegisterScreen";
import AboutScreen from './screens/AboutScreen';
import EventDetailScreen from './screens/EventDetailScreen';
import EventListScreen from './screens/EventListScreen';
import LoginScreen from './screens/LoginScreen';
import MembershipScreen from './screens/MembershipScreen';
import UserRegisterScreen from './screens/UserRegisterScreen';
const tabBarRoutes = {
EventList: EventListScreen,
Membership: MembershipScreen,
About: AboutScreen,
EventList: EventListScreen,
Membership: MembershipScreen,
About: AboutScreen,
};
const tabBarOptions = {
tabBarPosition: 'bottom',
tabBarOptions: {
style: {
...Platform.select({
android: {
backgroundColor: "#fff",
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: 'rgba(0, 0, 0, 0.1)'
}
})
},
indicatorStyle: {
backgroundColor: "#f58220"
},
activeTintColor: '#f58220',
inactiveTintColor: '#333333',
showIcon: true,
labelStyle: {
fontSize: 10,
marginBottom: 0,
...Platform.select({
android: {
fontWeight: 'bold',
}
}),
tabBarPosition: 'bottom',
tabBarOptions: {
style: {
...Platform.select({
android: {
backgroundColor: '#fff',
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: 'rgba(0, 0, 0, 0.1)',
},
}),
},
navigationOptions: ({ navigation, screenProps }) => ({
// get header title from active child screen
...getActiveChildNavigationOptions(navigation, screenProps),
headerStyle: {
backgroundColor: '#f58220',
indicatorStyle: {
backgroundColor: '#f58220',
},
activeTintColor: '#f58220',
inactiveTintColor: '#333333',
showIcon: true,
labelStyle: {
fontSize: 10,
marginBottom: 0,
...Platform.select({
android: {
fontWeight: 'bold',
},
headerTitleStyle: {
color: 'white',
},
headerTintColor: 'white',
})
}),
},
},
navigationOptions: ({ navigation, screenProps }) => ({
// get header title from active child screen
...getActiveChildNavigationOptions(navigation, screenProps),
headerStyle: {
backgroundColor: '#f58220',
},
headerTitleStyle: {
color: 'white',
},
headerTintColor: 'white',
}),
};
const tabNav = createBottomTabNavigator(tabBarRoutes, tabBarOptions);
const DuskenNavigation = createStackNavigator({
Root: tabNav,
Login: LoginScreen,
UserRegister: UserRegisterScreen,
EventDetail: EventDetailScreen,
Root: tabNav,
Login: LoginScreen,
UserRegister: UserRegisterScreen,
EventDetail: EventDetailScreen,
});
export default createAppContainer(DuskenNavigation);
+107 -107
View File
@@ -1,133 +1,133 @@
import {
LOGIN_REQUEST,
LOGIN_SUCCESS,
LOGIN_FAILURE,
LOGOUT,
USER_DATA_REQUEST,
USER_DATA_SUCCESS,
USER_DATA_FAILURE,
REGISTER_USER_REQUEST,
REGISTER_USER_FAILURE,
REGISTER_USER_SUCCESS, MEMBERSHIP_CHARGE_REQUEST, MEMBERSHIP_CHARGE_FAILURE, MEMBERSHIP_CHARGE_SUCCESS,
LOGIN_REQUEST,
LOGIN_SUCCESS,
LOGIN_FAILURE,
LOGOUT,
USER_DATA_REQUEST,
USER_DATA_SUCCESS,
USER_DATA_FAILURE,
REGISTER_USER_REQUEST,
REGISTER_USER_FAILURE,
REGISTER_USER_SUCCESS,
MEMBERSHIP_CHARGE_REQUEST,
MEMBERSHIP_CHARGE_FAILURE,
MEMBERSHIP_CHARGE_SUCCESS,
} from './actions';
import { snakeToCamelCase } from './utils'
import { snakeToCamelCase } from './utils';
const initialState = {
isAuthenticated: false,
isLoggingIn: false,
isFetchingUserData: false,
isRegisteringUser: false,
isChargingMembership: false,
userToken: null,
user: null,
loginError: null,
userError: null,
registerError: null,
chargeError: null,
lastOrder: null,
isAuthenticated: false,
isLoggingIn: false,
isFetchingUserData: false,
isRegisteringUser: false,
isChargingMembership: false,
userToken: null,
user: null,
loginError: null,
userError: null,
registerError: null,
chargeError: null,
lastOrder: null,
};
function formatErrors(errs) {
let _errs = {};
for(let [key, value] of Object.entries(errs)) {
_errs[snakeToCamelCase(key)] = Array.isArray(value) ? value.join('\n') : value;
}
return _errs
const _errs = {};
for (const [key, value] of Object.entries(errs)) {
_errs[snakeToCamelCase(key)] = Array.isArray(value) ? value.join('\n') : value;
}
return _errs;
}
export default function duskenApp(state = initialState, action) {
switch (action.type) {
/* Login */
case LOGIN_REQUEST:
return Object.assign({}, state, {
isLoggingIn: true,
loginError: null,
});
switch (action.type) {
/* Login */
case LOGIN_REQUEST:
return Object.assign({}, state, {
isLoggingIn: true,
loginError: null,
});
case LOGIN_FAILURE:
return Object.assign({}, state, {
isLoggingIn: false,
loginError: action.loginError
});
case LOGIN_FAILURE:
return Object.assign({}, state, {
isLoggingIn: false,
loginError: action.loginError,
});
case LOGIN_SUCCESS:
return Object.assign({}, state, {
isLoggingIn: false,
isAuthenticated: true,
userToken: action.token
});
case LOGIN_SUCCESS:
return Object.assign({}, state, {
isLoggingIn: false,
isAuthenticated: true,
userToken: action.token,
});
case LOGOUT:
return initialState;
case LOGOUT:
return initialState;
/* User data */
case USER_DATA_REQUEST:
return Object.assign({}, state, {
isFetchingUserData: true,
userError: null
});
/* User data */
case USER_DATA_REQUEST:
return Object.assign({}, state, {
isFetchingUserData: true,
userError: null,
});
case USER_DATA_FAILURE:
return Object.assign({}, state, {
isFetchingUserData: false,
userError: action.userError
});
case USER_DATA_FAILURE:
return Object.assign({}, state, {
isFetchingUserData: false,
userError: action.userError,
});
case USER_DATA_SUCCESS:
return Object.assign({}, state, {
isFetchingUserData: false,
user: action.data,
});
case USER_DATA_SUCCESS:
return Object.assign({}, state, {
isFetchingUserData: false,
user: action.data,
});
/* Register user */
case REGISTER_USER_REQUEST:
return Object.assign({}, state, {
isRegisteringUser: true,
registerError: null,
});
/* Register user */
case REGISTER_USER_REQUEST:
return Object.assign({}, state, {
isRegisteringUser: true,
registerError: null,
});
case REGISTER_USER_FAILURE:
return Object.assign({}, state, {
isRegisteringUser: false,
registerError: formatErrors(action.registerError)
});
case REGISTER_USER_FAILURE:
return Object.assign({}, state, {
isRegisteringUser: false,
registerError: formatErrors(action.registerError),
});
case REGISTER_USER_SUCCESS:
let userData = action.data;
const token = action.data.auth_token;
delete userData['auth_token'];
case REGISTER_USER_SUCCESS:
const userData = action.data;
const token = action.data.auth_token;
delete userData.auth_token;
return Object.assign({}, state, {
isRegisteringUser: false,
isAuthenticated: true,
user: userData,
userToken: token,
});
return Object.assign({}, state, {
isRegisteringUser: false,
isAuthenticated: true,
user: userData,
userToken: token,
});
/* User data */
case MEMBERSHIP_CHARGE_REQUEST:
return Object.assign({}, state, {
isChargingMembership: true,
chargeError: null
});
/* User data */
case MEMBERSHIP_CHARGE_REQUEST:
return Object.assign({}, state, {
isChargingMembership: true,
chargeError: null,
});
case MEMBERSHIP_CHARGE_FAILURE:
return Object.assign({}, state, {
isChargingMembership: false,
chargeError: action.chargeError
});
case MEMBERSHIP_CHARGE_FAILURE:
return Object.assign({}, state, {
isChargingMembership: false,
chargeError: action.chargeError,
});
case MEMBERSHIP_CHARGE_SUCCESS:
return Object.assign({}, state, {
isChargingMembership: false,
lastOrder: action.data,
});
case MEMBERSHIP_CHARGE_SUCCESS:
return Object.assign({}, state, {
isChargingMembership: false,
lastOrder: action.data,
});
default:
return state
}
default:
return state;
}
}
+5 -14
View File
@@ -1,26 +1,17 @@
import { applyMiddleware, createStore } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import AsyncStorage from '@react-native-community/async-storage';
import thunk from "redux-thunk";
import rootReducer from './reducers'
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const persistConfig = {
key: 'root',
storage: AsyncStorage,
blacklist: [
'isLoggingIn',
'isFetchingUserData',
'isRegisteringUser',
'isChargingMembership'
]
blacklist: ['isLoggingIn', 'isFetchingUserData', 'isRegisteringUser', 'isChargingMembership'],
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export let store = createStore(
persistedReducer,
applyMiddleware(...[thunk])
);
export let persistor = persistStore(store);
export const store = createStore(persistedReducer, applyMiddleware(...[thunk]));
export const persistor = persistStore(store);
+13 -14
View File
@@ -1,22 +1,21 @@
import React, { Component } from 'react';
import Icon from 'react-native-vector-icons/SimpleLineIcons';
import {StyleProvider} from "native-base";
import { StyleProvider } from 'native-base';
import AboutContainer from "../modules/about/AboutContainer";
import AboutContainer from '../modules/about/AboutContainer';
import getTheme from '../../native-base-theme/components';
export default class AboutScreen extends Component {
static navigationOptions = {
title: 'Om oss',
tabBarIcon: ({ tintColor }) => (
<Icon name="info" size={16} color={tintColor} style={{marginTop: 5}} />
),
};
static navigationOptions = {
title: 'Om oss',
tabBarIcon: ({ tintColor }) => <Icon name="info" size={16} color={tintColor} style={{ marginTop: 5 }} />,
};
render() {
return (
<StyleProvider style={getTheme()}>
<AboutContainer />
</StyleProvider>);
}
render() {
return (
<StyleProvider style={getTheme()}>
<AboutContainer />
</StyleProvider>
);
}
}
+21 -21
View File
@@ -1,26 +1,26 @@
import React, { Component } from 'react';
import {StyleProvider} from "native-base";
import EventDetail from "../modules/events/EventDetail";
import { StyleProvider } from 'native-base';
import EventDetail from '../modules/events/EventDetail';
import getTheme from '../../native-base-theme/components';
export default class EventDetailScreen extends Component {
static navigationOptions = ({navigation}) => ({
title: navigation.state.params.item.title.decoded,
headerStyle: {
backgroundColor: '#f58220',
},
headerTitleStyle: {
color: 'white',
},
headerTintColor: 'white',
});
static navigationOptions = ({ navigation }) => ({
title: navigation.state.params.item.title.decoded,
headerStyle: {
backgroundColor: '#f58220',
},
headerTitleStyle: {
color: 'white',
},
headerTintColor: 'white',
});
render() {
const { params } = this.props.navigation.state;
return (
<StyleProvider style={getTheme()}>
<EventDetail item={params.item}/>
</StyleProvider>
);
}
}
render() {
const { params } = this.props.navigation.state;
return (
<StyleProvider style={getTheme()}>
<EventDetail item={params.item} />
</StyleProvider>
);
}
}
+17 -19
View File
@@ -1,27 +1,25 @@
import React, { Component } from 'react';
import EventListContainer from "../modules/events/EventListContainer";
import Icon from 'react-native-vector-icons/SimpleLineIcons';
import {StyleProvider} from "native-base";
import { StyleProvider } from 'native-base';
import EventListContainer from '../modules/events/EventListContainer';
import getTheme from '../../native-base-theme/components';
export default class EventsScreen extends Component {
static navigationOptions = {
title: 'Program',
tabBarIcon: ({ tintColor }) => (
<Icon name="event" size={16} color={tintColor} style={{marginTop: 5}}/>
),
};
static navigationOptions = {
title: 'Program',
tabBarIcon: ({ tintColor }) => <Icon name="event" size={16} color={tintColor} style={{ marginTop: 5 }} />,
};
showEvent = (item) => {
const { navigate } = this.props.navigation;
navigate('EventDetail', {item: item})
};
showEvent = (item) => {
const { navigate } = this.props.navigation;
navigate('EventDetail', { item });
};
render() {
return (
<StyleProvider style={getTheme()}>
<EventListContainer showEvent={this.showEvent}/>
</StyleProvider>
);
}
render() {
return (
<StyleProvider style={getTheme()}>
<EventListContainer showEvent={this.showEvent} />
</StyleProvider>
);
}
}
+24 -24
View File
@@ -1,30 +1,30 @@
import React, { Component } from 'react';
import LoginContainer from "../modules/login/LoginContainer";
import {StyleProvider} from "native-base";
import { StyleProvider } from 'native-base';
import LoginContainer from '../modules/login/LoginContainer';
import getTheme from '../../native-base-theme/components';
export default class LoginScreen extends Component {
static navigationOptions = {
title: 'Logg inn',
headerStyle: {
backgroundColor: '#f58220',
},
headerTitleStyle: {
color: 'white',
},
headerTintColor: 'white',
};
static navigationOptions = {
title: 'Logg inn',
headerStyle: {
backgroundColor: '#f58220',
},
headerTitleStyle: {
color: 'white',
},
headerTintColor: 'white',
};
onLogin = () => {
// This works since LoginScreen is allways navigated to from MembershipScreen
this.props.navigation.goBack();
};
onLogin = () => {
// This works since LoginScreen is allways navigated to from MembershipScreen
this.props.navigation.goBack();
};
render() {
return (
<StyleProvider style={getTheme()}>
<LoginContainer onLogin={this.onLogin} />
</StyleProvider>
)
}
}
render() {
return (
<StyleProvider style={getTheme()}>
<LoginContainer onLogin={this.onLogin} />
</StyleProvider>
);
}
}
+26 -27
View File
@@ -1,37 +1,36 @@
import React, { Component } from 'react';
import MembershipContainer from "../modules/membership/MembershipContainer";
import Icon from 'react-native-vector-icons/SimpleLineIcons';
import {StyleProvider} from "native-base";
import { StyleProvider } from 'native-base';
import MembershipContainer from '../modules/membership/MembershipContainer';
import getTheme from '../../native-base-theme/components';
export default class MembershipScreen extends Component {
static navigationOptions = {
title: 'Medlemskap',
tabBarIcon: ({ tintColor }) => (
<Icon name="user" size={16} color={tintColor} style={{marginTop: 5}} />
),
};
static navigationOptions = {
title: 'Medlemskap',
tabBarIcon: ({ tintColor }) => <Icon name="user" size={16} color={tintColor} style={{ marginTop: 5 }} />,
};
loginNavigate = () => {
this.props.navigation.navigate('Login');
};
loginNavigate = () => {
this.props.navigation.navigate('Login');
};
logoutNavigate = () => {
this.props.navigation.navigate('Membership');
};
logoutNavigate = () => {
this.props.navigation.navigate('Membership');
};
registerNavigate = () => {
this.props.navigation.navigate('UserRegister');
};
registerNavigate = () => {
this.props.navigation.navigate('UserRegister');
};
render() {
return (
<StyleProvider style={getTheme()}>
<MembershipContainer
onLoginPress={this.loginNavigate}
onLogoutPress={this.logoutNavigate}
onRegisterPress={this.registerNavigate} />
</StyleProvider>
)
}
render() {
return (
<StyleProvider style={getTheme()}>
<MembershipContainer
onLoginPress={this.loginNavigate}
onLogoutPress={this.logoutNavigate}
onRegisterPress={this.registerNavigate}
/>
</StyleProvider>
);
}
}
+24 -24
View File
@@ -1,30 +1,30 @@
import React, { Component } from 'react';
import UserRegisterContainer from "../modules/register/UserRegisterContainer";
import {StyleProvider} from "native-base";
import { StyleProvider } from 'native-base';
import UserRegisterContainer from '../modules/register/UserRegisterContainer';
import getTheme from '../../native-base-theme/components';
export default class UserRegisterScreen extends Component {
static navigationOptions = {
title: 'Bli medlem',
headerStyle: {
backgroundColor: '#f58220',
},
headerTitleStyle: {
color: 'white',
},
headerTintColor: 'white',
};
static navigationOptions = {
title: 'Bli medlem',
headerStyle: {
backgroundColor: '#f58220',
},
headerTitleStyle: {
color: 'white',
},
headerTintColor: 'white',
};
onRegister = () => {
// This works since UserRegisterScreen is allways navigated to from MembershipScreen
this.props.navigation.goBack();
};
onRegister = () => {
// This works since UserRegisterScreen is allways navigated to from MembershipScreen
this.props.navigation.goBack();
};
render() {
return (
<StyleProvider style={getTheme()}>
<UserRegisterContainer onRegister={this.onRegister} />
</StyleProvider>
)
}
}
render() {
return (
<StyleProvider style={getTheme()}>
<UserRegisterContainer onRegister={this.onRegister} />
</StyleProvider>
);
}
}
+18 -18
View File
@@ -1,24 +1,24 @@
import { Platform } from "react-native";
import platform from "native-base/src/theme/variables/platform";
import { Platform } from 'react-native';
import platform from 'native-base/src/theme/variables/platform';
const colors = {
danger: platform.btnDangerBg
danger: platform.btnDangerBg,
};
const card = {
...Platform.select({
ios: {
shadowColor: 'rgba(0,0,0, .2)',
shadowOffset: {height: 0, width: 0},
shadowOpacity: 1,
shadowRadius: 1,
},
android: {
elevation: 1,
},
}),
padding: 8,
margin: 8,
backgroundColor: '#fff',
...Platform.select({
ios: {
shadowColor: 'rgba(0,0,0, .2)',
shadowOffset: { height: 0, width: 0 },
shadowOpacity: 1,
shadowRadius: 1,
},
android: {
elevation: 1,
},
}),
padding: 8,
margin: 8,
backgroundColor: '#fff',
};
export default {card, colors};
export default { card, colors };
+10 -10
View File
@@ -1,14 +1,14 @@
export function snakeToCamelCase(val) {
return val.replace(/_([a-z])/g, function (g) { return g[1].toUpperCase(); });
return val.replace(/_([a-z])/g, function(g) {
return g[1].toUpperCase();
});
}
export function fetchWithTimeout(url, options, timeout=10000) {
/** Wraps fetch to allow it to time out */
// FIXME: show timeout message in UI
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Fetch request timed out')), timeout)
)
]);
export function fetchWithTimeout(url, options, timeout = 10000) {
/** Wraps fetch to allow it to time out */
// FIXME: show timeout message in UI
return Promise.race([
fetch(url, options),
new Promise((_, reject) => setTimeout(() => reject(new Error('Fetch request timed out')), timeout)),
]);
}