diff --git a/.eslintrc b/.eslintrc
index 8c3380c..4f70013 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -12,6 +12,8 @@
"no-underscore-dangle": 0,
"no-param-reassign": 0,
"react/prefer-stateless-function": 1,
+ "react/prop-types": 1, // FIXME: refactor
+ "class-methods-use-this": 1, // FIXME: refactor
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"react/sort-comp": 0,
"max-len": 0,
diff --git a/android/fastlane/metadata/android/en-US/images/icon.png b/android/fastlane/metadata/android/en-US/images/icon.png
index e186674..fe8f0c5 100644
Binary files a/android/fastlane/metadata/android/en-US/images/icon.png and b/android/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/android/fastlane/metadata/android/no-NO/images/icon.png b/android/fastlane/metadata/android/no-NO/images/icon.png
index e186674..fe8f0c5 100644
Binary files a/android/fastlane/metadata/android/no-NO/images/icon.png and b/android/fastlane/metadata/android/no-NO/images/icon.png differ
diff --git a/src/App.js b/src/App.js
index 4e3b4e3..25bfb95 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/es/integration/react';
import Config from 'react-native-config';
@@ -15,16 +15,14 @@ const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
});
-export default class App extends Component {
- render() {
- return (
-
-
-
-
-
-
-
- );
- }
+export default function App() {
+ return (
+
+
+
+
+
+
+
+ );
}
diff --git a/src/Dusken.js b/src/Dusken.js
index 7744392..d6e666b 100644
--- a/src/Dusken.js
+++ b/src/Dusken.js
@@ -1,16 +1,12 @@
-import React, { Component } from 'react';
+import React from 'react';
import { connect } from 'react-redux';
import { Root } from 'native-base';
import DuskenNavigation from './navigation';
-class DuskenContainer extends Component {
- render() {
- return (
-
-
-
- );
- }
-}
+const DuskenContainer = () => (
+
+
+
+);
export default connect()(DuskenContainer);
diff --git a/src/api.js b/src/api.js
index dd96f18..bd1c621 100644
--- a/src/api.js
+++ b/src/api.js
@@ -51,7 +51,7 @@ export function requestLogin(username, password) {
};
}
-export function requestUserData(auth_token) {
+export function requestUserData(authToken) {
return (dispatch) => {
// We are now fetching user data
dispatch(userDataRequest());
@@ -61,7 +61,7 @@ export function requestUserData(auth_token) {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
- Authorization: `Token ${auth_token}`,
+ Authorization: `Token ${authToken}`,
},
})
.then((response) => response.json().then((json) => ({ json, response })))
@@ -118,7 +118,7 @@ export function requestRegisterUser(firstName, lastName, email, phoneNumber, pas
};
}
-export function requestMembershipCharge(auth_token, stripe_token, membership_type) {
+export function requestMembershipCharge(authToken, stripeToken, membershipType) {
return (dispatch) => {
dispatch(membershipChargeRequest());
@@ -127,11 +127,11 @@ export function requestMembershipCharge(auth_token, stripe_token, membership_typ
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
- Authorization: `Token ${auth_token}`,
+ Authorization: `Token ${authToken}`,
},
body: JSON.stringify({
- stripe_token,
- membership_type,
+ stripe_token: stripeToken,
+ membership_type: membershipType,
payment_method: 'app',
}),
})
diff --git a/src/modules/about/About.js b/src/modules/about/About.js
index 3fcb7cd..8999579 100644
--- a/src/modules/about/About.js
+++ b/src/modules/about/About.js
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
-import { Text, ScrollView, View, Platform, StyleSheet, Image, Linking, TouchableOpacity } from 'react-native';
-// import HTMLView from 'react-native-htmlview';
+import { Text, View, Platform, StyleSheet, Image, Linking, TouchableOpacity } from 'react-native';
+import { ScrollView } from 'react-navigation';
import { Button, Text as NBText, Icon } from 'native-base';
import theme from '../../theme';
@@ -140,8 +140,6 @@ export default class About extends Component {
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.
-
- {/* */}
);
diff --git a/src/modules/about/AboutContainer.js b/src/modules/about/AboutContainer.js
index dd811ce..47c8b7c 100644
--- a/src/modules/about/AboutContainer.js
+++ b/src/modules/about/AboutContainer.js
@@ -1,12 +1,7 @@
-import { connect } from 'react-redux';
-import React, { Component } from 'react';
+import React from 'react';
import About from './About';
-class AboutContainer extends Component {
- render() {
- return ;
- }
-}
+const AboutContainer = () => ;
-export default connect()(AboutContainer);
+export default AboutContainer;
diff --git a/src/modules/events/EventDetail.js b/src/modules/events/EventDetail.js
index 9426020..1fed83c 100644
--- a/src/modules/events/EventDetail.js
+++ b/src/modules/events/EventDetail.js
@@ -3,6 +3,7 @@ import { Linking, StyleSheet, ScrollView, Text, Platform, View, Image, Touchable
import HTMLView from 'react-native-htmlview';
import moment from 'moment';
import { Icon } from 'native-base';
+
import theme from '../../theme';
moment.locale('nb');
@@ -44,8 +45,8 @@ export default class EventDetail extends Component {
_formatTicketText() {
/* FIXME: This should be handled by API */
const { item } = this.props;
- let reg_kr = '';
- let mem_kr = '';
+ let regularPrice = '';
+ let memberPrice = '';
if (item.price_regular === '0') {
item.price_regular = 'Gratis';
}
@@ -53,19 +54,19 @@ export default class EventDetail extends Component {
item.price_member = 'Gratis';
}
if (item.price_regular !== 'Gratis') {
- reg_kr = ' kr';
+ regularPrice = ' kr';
}
if (item.price_member !== 'Gratis') {
- mem_kr = ' kr';
+ memberPrice = ' kr';
}
let text = '';
if (item.price_regular && item.price_member) {
- text = `Pris: ${item.price_regular}${reg_kr} / Medlemmer: ${item.price_member}${mem_kr}`;
+ text = `Pris: ${item.price_regular}${regularPrice} / Medlemmer: ${item.price_member}${memberPrice}`;
} else if (item.price_regular) {
- text = `Pris: ${item.price_regular}${reg_kr}`;
+ text = `Pris: ${item.price_regular}${regularPrice}`;
} else if (item.price_member) {
- text = `Pris (Medlemmer): ${item.price_member}${mem_kr}`;
+ text = `Pris (Medlemmer): ${item.price_member}${memberPrice}`;
} else {
return '';
}
@@ -108,7 +109,7 @@ export default class EventDetail extends Component {
const text = this._formatTicketText();
if (text === '') {
- return;
+ return null;
}
if (item.ticket_url) {
@@ -141,7 +142,7 @@ export default class EventDetail extends Component {
showFacebutton() {
const { item } = this.props;
if (!item.facebook_url) {
- return;
+ return null;
}
return (
@@ -162,6 +163,8 @@ export default class EventDetail extends Component {
if (item.thumbnail.medium_large) {
return ;
}
+
+ return null;
}
showYear() {
@@ -170,6 +173,8 @@ export default class EventDetail extends Component {
if (moment().year() !== moment(item.start_time).year()) {
return {this._formatYear(item.start_time)};
}
+
+ return null;
}
render() {
diff --git a/src/modules/events/EventList.js b/src/modules/events/EventList.js
index 57cd24c..fa5a307 100644
--- a/src/modules/events/EventList.js
+++ b/src/modules/events/EventList.js
@@ -1,6 +1,6 @@
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 { Card, ListItem, CardItem, Body, Text, Right, Icon, Button, Content, Spinner } from 'native-base';
import React, { Component } from 'react';
import moment from 'moment';
import 'moment/locale/nb';
@@ -64,11 +64,7 @@ export default class EventList extends Component {
return (
- {
- this._onPressItem(item);
- }}
- >
+
Kunne ikke hente programmet...
diff --git a/src/modules/events/EventListContainer.js b/src/modules/events/EventListContainer.js
index 680cc03..bad24d5 100644
--- a/src/modules/events/EventListContainer.js
+++ b/src/modules/events/EventListContainer.js
@@ -6,6 +6,7 @@ import moment from 'moment';
import 'moment/locale/nb';
import EventList from './EventList';
+import { fetchWithTimeout } from '../../utils';
moment.locale('nb');
@@ -35,7 +36,7 @@ class EventListContainer extends Component {
_toSectionFormat(events) {
const sectioned = {};
- events.map((event) => {
+ events.forEach((event) => {
const title = this._formatDate(event.start_time);
if (title in sectioned) {
sectioned[title].push(event);
@@ -84,9 +85,9 @@ class EventListContainer extends Component {
const url = `${Config.EVENT_API_URL}/wp-json/wp/v2/events?page=${page}&future=1`;
this.setState({ loading: true });
- fetch(url)
+ fetchWithTimeout(url)
.then((res) => {
- this.setState({ totalPages: parseInt(res.headers.get('x-wp-totalpages')) });
+ this.setState({ totalPages: parseInt(res.headers.get('x-wp-totalpages'), 10) });
return res.json();
})
.then((res) => {
diff --git a/src/modules/login/Login.js b/src/modules/login/Login.js
index 0f945da..2888a42 100644
--- a/src/modules/login/Login.js
+++ b/src/modules/login/Login.js
@@ -1,6 +1,6 @@
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 { Linking, StyleSheet, View } from 'react-native';
+import { Container, Content, Form, Item, Input, Label, Spinner, Button, Text } from 'native-base';
import Config from 'react-native-config';
import theme from '../../theme';
@@ -26,7 +26,7 @@ export default class Login extends Component {
returnKeyType="next"
onChangeText={this.handleEmail}
onSubmitEditing={() => {
- this.refs.passwordInput._root.focus();
+ this.passwordInputRef._root.focus();
}}
/>
@@ -44,7 +44,7 @@ export default class Login extends Component {
returnKeyType="next"
onChangeText={this.handleEmail}
onSubmitEditing={() => {
- this.refs.passwordInput._root.focus();
+ this.passwordInputRef._root.focus();
}}
/>
@@ -57,7 +57,9 @@ export default class Login extends Component {
-
{
+ this.passwordInputRef = ref;
+ }}
secureTextEntry={true}
autoCapitalize="none"
autoCorrect={false}
@@ -71,7 +73,9 @@ export default class Login extends Component {
-
{
+ this.passwordInputRef = ref;
+ }}
secureTextEntry={true}
autoCapitalize="none"
autoCorrect={false}
@@ -125,11 +129,7 @@ export default class Login extends Component {
return ;
};
- showSpinner = () => {
- if (this.props.isLoggingIn) {
- return ;
- }
- };
+ showSpinner = () => (this.props.isLoggingIn ? : null);
handleEmail = (text) => {
this.setState({ email: text });
diff --git a/src/modules/membership/Proof.js b/src/modules/membership/Proof.js
index 03f7011..e42d4e3 100644
--- a/src/modules/membership/Proof.js
+++ b/src/modules/membership/Proof.js
@@ -44,7 +44,7 @@ export default class Proof extends Component {
membershipValidTo() {
if (!this.props.user.last_membership) {
- return;
+ return null;
}
let validTo = this.props.user.last_membership.end_date;
@@ -63,7 +63,7 @@ export default class Proof extends Component {
membershipStatus() {
if (!this.props.user.is_member) {
if (!this.props.user.last_membership) {
- return;
+ return null;
}
return (
@@ -86,7 +86,7 @@ export default class Proof extends Component {
purchaseButton() {
if (this.props.user.is_member) {
- return;
+ return null;
}
return (
@@ -108,15 +108,24 @@ export default class Proof extends Component {
}
confetti() {
- if (this.props.user.is_member) {
- // Confetti!!!!
- return (this._confettiView = node)} confettiCount={Number.MAX_SAFE_INTEGER} />;
+ if (!this.props.user.is_member) {
+ return null;
}
+
+ // Confetti!!!!
+ return (
+ {
+ this._confettiView = node;
+ }}
+ confettiCount={Number.MAX_SAFE_INTEGER}
+ />
+ );
}
logo() {
if (!this.props.user.is_member) {
- return;
+ return null;
}
// FIXME: URL to config
@@ -133,7 +142,7 @@ export default class Proof extends Component {
chargeError() {
if (!this.props.chargeError) {
- return;
+ return null;
}
return (
diff --git a/src/modules/membership/ProofContainer.js b/src/modules/membership/ProofContainer.js
index 06c722a..8ef56e2 100644
--- a/src/modules/membership/ProofContainer.js
+++ b/src/modules/membership/ProofContainer.js
@@ -62,7 +62,7 @@ class ProofContainer extends Component {
membershipTypeSlug
);
},
- (error) => {
+ () => {
/* FIXME: Happens only if card dialog is canceled? */
}
);
diff --git a/src/modules/register/UserRegister.js b/src/modules/register/UserRegister.js
index 71babfb..42efe9f 100644
--- a/src/modules/register/UserRegister.js
+++ b/src/modules/register/UserRegister.js
@@ -1,7 +1,7 @@
import React, { Component } from 'react';
-import { Container, Header, Content, Form, Item, Input, Label, Spinner, Button, Text, Icon, Toast } from 'native-base';
+import { Container, 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 * as EmailValidator from 'email-validator';
import theme from '../../theme';
@@ -27,7 +27,7 @@ export default class UserRegister extends Component {
const touched = new Set(this.state.touched).add(triggeredBy);
/* No empty fields */
- for (const key of fieldNames) {
+ fieldNames.forEach((key) => {
const value = this.state[key];
if (value === '') {
errors[key] = 'kan ikke være tomt';
@@ -36,7 +36,7 @@ export default class UserRegister extends Component {
} else {
delete errors[key];
}
- }
+ });
this.setState({ errors, touched });
}
@@ -62,7 +62,7 @@ export default class UserRegister extends Component {
});
}}
onSubmitEditing={() => {
- this.refs.lastNameInput._root.focus();
+ this.lastNameInputRef._root.focus();
}}
value={this.state.firstName}
/>
@@ -76,7 +76,9 @@ export default class UserRegister extends Component {
-
{
+ this.lastNameInputRef = ref;
+ }}
returnKeyType="next"
onChangeText={(lastName) => {
this.setState({ lastName }, () => {
@@ -84,7 +86,7 @@ export default class UserRegister extends Component {
});
}}
onSubmitEditing={() => {
- this.refs.emailInput._root.focus();
+ this.emailInputRef._root.focus();
}}
value={this.state.lastName}
/>
@@ -98,7 +100,9 @@ export default class UserRegister extends Component {
-
{
+ this.emailInputRef = ref;
+ }}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
@@ -109,7 +113,7 @@ export default class UserRegister extends Component {
});
}}
onSubmitEditing={() => {
- this.refs.phoneInput._root.focus();
+ this.phoneInputRef._root.focus();
}}
value={this.state.email}
/>
@@ -123,7 +127,9 @@ export default class UserRegister extends Component {
-
{
+ this.phoneInputRef = ref;
+ }}
returnKeyType="next"
keyboardType="phone-pad"
onChangeText={(phoneNumber) => {
@@ -132,7 +138,7 @@ export default class UserRegister extends Component {
});
}}
onSubmitEditing={() => {
- this.refs.passwordInput._root.focus();
+ this.passwordInputRef._root.focus();
}}
value={this.state.phoneNumber}
/>
@@ -146,7 +152,9 @@ export default class UserRegister extends Component {
-
{
+ this.passwordInputRef = ref;
+ }}
secureTextEntry={true}
autoCapitalize="none"
autoCorrect={false}
@@ -195,7 +203,7 @@ export default class UserRegister extends Component {
showFieldError = (field) => {
if (!this.fieldHasError(field)) {
- return;
+ return null;
}
return (
-
@@ -205,9 +213,10 @@ export default class UserRegister extends Component {
};
showSpinner = () => {
- if (this.props.isRegisteringUser) {
- return ;
+ if (!this.props.isRegisteringUser) {
+ return null;
}
+ return ;
};
render() {
diff --git a/src/navigation.js b/src/navigation.js
index e04782e..7184d39 100644
--- a/src/navigation.js
+++ b/src/navigation.js
@@ -62,11 +62,24 @@ const tabBarOptions = {
const tabNav = createBottomTabNavigator(tabBarRoutes, tabBarOptions);
-const DuskenNavigation = createStackNavigator({
- Root: tabNav,
- Login: LoginScreen,
- UserRegister: UserRegisterScreen,
- EventDetail: EventDetailScreen,
-});
+const DuskenNavigation = createStackNavigator(
+ {
+ Root: tabNav,
+ Login: LoginScreen,
+ UserRegister: UserRegisterScreen,
+ EventDetail: EventDetailScreen,
+ },
+ {
+ defaultNavigationOptions: {
+ headerStyle: {
+ backgroundColor: '#f58220',
+ },
+ headerTitleStyle: {
+ color: 'white',
+ },
+ headerTintColor: 'white',
+ },
+ }
+);
export default createAppContainer(DuskenNavigation);
diff --git a/src/reducers.js b/src/reducers.js
index aaf9731..31b17f2 100644
--- a/src/reducers.js
+++ b/src/reducers.js
@@ -33,9 +33,9 @@ const initialState = {
function formatErrors(errs) {
const _errs = {};
- for (const [key, value] of Object.entries(errs)) {
+ Object.entries(errs).forEach(([key, value]) => {
_errs[snakeToCamelCase(key)] = Array.isArray(value) ? value.join('\n') : value;
- }
+ });
return _errs;
}
@@ -97,7 +97,9 @@ export default function duskenApp(state = initialState, action) {
});
case REGISTER_USER_SUCCESS:
+ // eslint-disable-next-line no-case-declarations
const userData = action.data;
+ // eslint-disable-next-line no-case-declarations
const token = action.data.auth_token;
delete userData.auth_token;
diff --git a/src/screens/AboutScreen.js b/src/screens/AboutScreen.js
index 65a4be9..c6958c4 100644
--- a/src/screens/AboutScreen.js
+++ b/src/screens/AboutScreen.js
@@ -1,21 +1,21 @@
-import React, { Component } from 'react';
+import React from 'react';
import Icon from 'react-native-vector-icons/SimpleLineIcons';
import { StyleProvider } from 'native-base';
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 }) => ,
- };
+const AboutScreen = () => (
+
+
+
+);
- render() {
- return (
-
-
-
- );
- }
-}
+const TabBarIcon = ({ tintColor }) => ;
+
+AboutScreen.navigationOptions = {
+ title: 'Om oss',
+ tabBarIcon: TabBarIcon,
+};
+
+export default AboutScreen;
diff --git a/src/screens/EventDetailScreen.js b/src/screens/EventDetailScreen.js
index 3697aeb..267ae4d 100644
--- a/src/screens/EventDetailScreen.js
+++ b/src/screens/EventDetailScreen.js
@@ -1,26 +1,21 @@
-import React, { Component } from 'react';
+import React from 'react';
import { StyleProvider } from 'native-base';
+import { useNavigationParam } from 'react-navigation-hooks/src/Hooks';
+
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',
- });
+const EventDetailScreen = () => {
+ const item = useNavigationParam('item');
- render() {
- const { params } = this.props.navigation.state;
- return (
-
-
-
- );
- }
-}
+ return (
+
+
+
+ );
+};
+EventDetailScreen.navigationOptions = ({ navigation }) => ({
+ title: navigation.state.params.item.title.decoded,
+});
+
+export default EventDetailScreen;
diff --git a/src/screens/EventListScreen.js b/src/screens/EventListScreen.js
index a911692..5f07d93 100644
--- a/src/screens/EventListScreen.js
+++ b/src/screens/EventListScreen.js
@@ -1,25 +1,27 @@
-import React, { Component } from 'react';
+import React from 'react';
import Icon from 'react-native-vector-icons/SimpleLineIcons';
import { StyleProvider } from 'native-base';
+import { useNavigation } from 'react-navigation-hooks';
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 }) => ,
- };
+const EventsScreen = () => {
+ const { navigate } = useNavigation();
+ return (
+
+ {
+ navigate('EventDetail', { item });
+ }}
+ />
+
+ );
+};
- showEvent = (item) => {
- const { navigate } = this.props.navigation;
- navigate('EventDetail', { item });
- };
+const tabBarIcon = ({ tintColor }) => ;
- render() {
- return (
-
-
-
- );
- }
-}
+EventsScreen.navigationOptions = {
+ title: 'Program',
+ tabBarIcon,
+};
+export default EventsScreen;
diff --git a/src/screens/LoginScreen.js b/src/screens/LoginScreen.js
index a12ba87..2aa43f8 100644
--- a/src/screens/LoginScreen.js
+++ b/src/screens/LoginScreen.js
@@ -1,30 +1,19 @@
-import React, { Component } from 'react';
+import React from 'react';
import { StyleProvider } from 'native-base';
+import { useNavigation } from 'react-navigation-hooks';
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',
- };
-
- onLogin = () => {
- // This works since LoginScreen is allways navigated to from MembershipScreen
- this.props.navigation.goBack();
- };
-
- render() {
- return (
-
-
-
- );
- }
-}
+// navigation.goBack onLogin works since LoginScreen is allways navigated to from MembershipScreen
+const LoginScreen = () => {
+ const navigation = useNavigation();
+ return (
+
+
+
+ );
+};
+LoginScreen.navigationOptions = {
+ title: 'Logg inn',
+};
+export default LoginScreen;
diff --git a/src/screens/UserRegisterScreen.js b/src/screens/UserRegisterScreen.js
index 8cfc626..d63c0fa 100644
--- a/src/screens/UserRegisterScreen.js
+++ b/src/screens/UserRegisterScreen.js
@@ -6,13 +6,6 @@ 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',
};
onRegister = () => {
diff --git a/src/utils.js b/src/utils.js
index cd3930b..adb09b6 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,3 +1,4 @@
+/* global fetch */
export function snakeToCamelCase(val) {
return val.replace(/_([a-z])/g, function(g) {
return g[1].toUpperCase();