diff --git a/lab-nathan/backend b/lab-nathan/backend new file mode 160000 index 0000000..1fc6e99 --- /dev/null +++ b/lab-nathan/backend @@ -0,0 +1 @@ +Subproject commit 1fc6e992ae12d84d6e3f8dfc67a18c9f2eca29ad diff --git a/lab-nathan/frontend/.babelrc b/lab-nathan/frontend/.babelrc new file mode 100644 index 0000000..cf6ae40 --- /dev/null +++ b/lab-nathan/frontend/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "react"], + "plugins": ["transform-object-rest-spread"] +} \ No newline at end of file diff --git a/lab-nathan/frontend/.gitignore b/lab-nathan/frontend/.gitignore new file mode 100644 index 0000000..60a5626 --- /dev/null +++ b/lab-nathan/frontend/.gitignore @@ -0,0 +1,4 @@ +node_modules +coverage +build +.env \ No newline at end of file diff --git a/lab-nathan/frontend/package.json b/lab-nathan/frontend/package.json new file mode 100644 index 0000000..4e62764 --- /dev/null +++ b/lab-nathan/frontend/package.json @@ -0,0 +1,44 @@ +{ + "name": "frontend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "webpack", + "watch": "webpack-dev-server --inline --hot", + "test": "jest" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "babel-core": "^6.26.0", + "babel-loader": "^7.1.2", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-preset-es2015": "^6.24.1", + "babel-preset-react": "^6.24.1", + "clean-webpack-plugin": "^0.1.16", + "css-loader": "^0.28.7", + "dotenv": "^4.0.0", + "extract-text-webpack-plugin": "^3.0.0", + "file-loader": "^0.11.2", + "html-webpack-plugin": "^2.30.1", + "jest": "^21.0.1", + "node-sass": "^4.5.3", + "raw-loader": "^0.5.1", + "react": "^15.6.1", + "react-dom": "^15.6.1", + "react-redux": "^5.0.6", + "react-router": "^4.2.0", + "react-router-dom": "^4.2.2", + "redux": "^3.7.2", + "redux-mock-store": "^1.2.3", + "sass-loader": "^6.0.6", + "superagent": "^3.6.0", + "uglifyjs-webpack-plugin": "^0.4.6", + "url-loader": "^0.5.9", + "uuid": "^3.1.0", + "webpack": "^3.5.6", + "webpack-dev-server": "^2.7.1" + } +} diff --git a/lab-nathan/frontend/src/__test__/token-reducer.test.js b/lab-nathan/frontend/src/__test__/token-reducer.test.js new file mode 100644 index 0000000..0e0ca69 --- /dev/null +++ b/lab-nathan/frontend/src/__test__/token-reducer.test.js @@ -0,0 +1,24 @@ +import tokenReducer from '../reducers/token-reducer.js'; + +describe('Token Reducer', function() { + it('TOKEN_SET should return a token.', function() { + let testAction = { + type: 'TOKEN_SET', + payload: 'kasdf;kasdfj;asdfj;s' + }; + + let result = tokenReducer(null, testAction); + + expect(result).toEqual(testAction.payload); + }); + + it('TOKEN_DELETE should delete a token.', function() { + let testAction = { + type: 'TOKEN_DELETE' + }; + + let result = tokenReducer(null, testAction); + + expect(result).toEqual(null); + }) +}); \ No newline at end of file diff --git a/lab-nathan/frontend/src/actions/photo-actions.js b/lab-nathan/frontend/src/actions/photo-actions.js new file mode 100644 index 0000000..367e78b --- /dev/null +++ b/lab-nathan/frontend/src/actions/photo-actions.js @@ -0,0 +1,67 @@ +import superagent from 'superagent' + +// sync actions +export const userPhotosSet = (photos) => ({ + type: 'USER_PHOTOS_SET', + payload: photos, +}) + +export const userPhotoCreate = (photo) => ({ + type: 'USER_PHOTO_CREATE', + payload: photo, +}) + +export const userPhotoUpdate = (photo) => ({ + type: 'USER_PHOTO_UPDATE', + payload: photo, +}) + +export const userPhotoDelete = (photo) => ({ + type: 'USER_PHOTO_DELETE', + payload: photo, +}) + +// async actions +export const userPhotosFetchRequest = () => (dispatch, getState) => { + let {token} = getState() + return superagent.get(`${__API_URL__}/photos/me`) + .set('Authorization', `Bearer ${token}`) + .then(res => { + dispatch(userPhotosSet(res.body.data)) + return res + }) +} + +export const userPhotoCreateRequest = (photo) => (dispatch, getState) => { + let {token} = getState() + return superagent.post(`${__API_URL__}/photos`) + .set('Authorization', `Bearer ${token}`) + .field('description', photo.description) + .attach('photo', photo.photo) + .then((res) => { + dispatch(userPhotoCreate(res.body)) + return res + }) +} + +export const userPhotoDeleteRequest = (photo) => (dispatch, getState) => { + let {token} = getState() + return superagent.delete(`${__API_URL__}/photos/${photo._id}`) + .set('Authorization', `Bearer ${token}`) + .then(res => { + dispatch(userPhotoDelete(photo)) + return res + }) +} + +export const userPhotoUpdateRequest = (photo) => (dispatch, getState) => { + let {token} = getState() + return superagent.put(`${__API_URL__}/photos/${photo._id}`) + .set('Authorization', `Bearer ${token}`) + .field('description', photo.description) + .attach('photo', photo.photo) + .then(res => { + dispatch(userPhotoUpdate(res.body)) + return res + }) +} diff --git a/lab-nathan/frontend/src/actions/profile-actions.js b/lab-nathan/frontend/src/actions/profile-actions.js new file mode 100644 index 0000000..06409cf --- /dev/null +++ b/lab-nathan/frontend/src/actions/profile-actions.js @@ -0,0 +1,48 @@ +import superagent from 'superagent' + +// sync action creators +export const profileCreate = (profile) => ({ + type: 'PROFILE_CREATE', + payload: profile, +}) + +export const profileUpdate = (profile) => ({ + type: 'PROFILE_UPDATE', + payload: profile, +}) + +// async action creators +export const profileCreateRequest = (profile) => (dispatch, getState) => { + let {token} = getState() + return superagent.post(`${__API_URL__}/profiles`) + .set('Authorization', `Bearer ${token}`) + .field('bio', profile.bio) + .attach('avatar', profile.avatar) + .then(res => { + dispatch(profileCreate(res.body)) + return res + }) +} + +export const profileUpdateRequest = (profile) => (dispatch, getState) => { + let {token} = getState() + return superagent.put(`${__API_URL__}/profiles/${profile._id}`) + .set('Authorization', `Bearer ${token}`) + .field('bio', profile.bio) + .attach('avatar', profile.avatar) + .then(res => { + dispatch(profileCreate(res.body)) + return res + }) +} + +export const profileFetchRequest = () => (dispatch, getState) => { + let {token} = getState() + return superagent.get(`${__API_URL__}/profiles/me`) + .set('Authorization', `Bearer ${token}`) + .then(res => { + console.log(res); + dispatch(profileCreate(res.body)) + return res + }) +} diff --git a/lab-nathan/frontend/src/actions/token-actions.js b/lab-nathan/frontend/src/actions/token-actions.js new file mode 100644 index 0000000..a81198b --- /dev/null +++ b/lab-nathan/frontend/src/actions/token-actions.js @@ -0,0 +1,43 @@ +import superagent from 'superagent'; +import * as util from '../lib/utilities.js'; + +export const tokenSet = (token) => ({ + type: 'TOKEN_SET', + payload: token +}); + +export const tokenDelete = (token) => ({ + type: 'TOKEN_DELETE' +}); + +export const logout = () => { + util.deleteCookie('X-Sluggram-Token') + return { type: 'LOGOUT' } +} + +export const signupRequest = (user) => (dispatch) => { + return superagent.post(`${__API_URL__}/signup`) + .withCredentials() + .send(user) + .then(response => { + dispatch(tokenSet(response.text)); + try { + localStorage.token = response.text; + } + catch (error) { + console.log(error); + } + + return response; + }); +}; + +export const loginRequest = (user) => (dispatch) => { + return superagent.get(`${__API_URL__}/login`) + .withCredentials() + .auth(user.username, user.password) + .then(response => { + dispatch(tokenSet(response.text)) + return response; + }); +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/assets/btn_google_dark_disabled_ios.svg b/lab-nathan/frontend/src/assets/btn_google_dark_disabled_ios.svg new file mode 100644 index 0000000..ecda263 --- /dev/null +++ b/lab-nathan/frontend/src/assets/btn_google_dark_disabled_ios.svg @@ -0,0 +1,24 @@ + + + + btn_google_dark_disabled_ios + Created with Sketch. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lab-nathan/frontend/src/assets/btn_google_dark_focus_ios.svg b/lab-nathan/frontend/src/assets/btn_google_dark_focus_ios.svg new file mode 100644 index 0000000..dbf2c96 --- /dev/null +++ b/lab-nathan/frontend/src/assets/btn_google_dark_focus_ios.svg @@ -0,0 +1,51 @@ + + + + btn_google_dark_focus_ios + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lab-nathan/frontend/src/assets/btn_google_dark_normal_ios.svg b/lab-nathan/frontend/src/assets/btn_google_dark_normal_ios.svg new file mode 100644 index 0000000..8464cb2 --- /dev/null +++ b/lab-nathan/frontend/src/assets/btn_google_dark_normal_ios.svg @@ -0,0 +1,50 @@ + + + + btn_google_dark_normal_ios + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lab-nathan/frontend/src/assets/btn_google_dark_pressed_ios.svg b/lab-nathan/frontend/src/assets/btn_google_dark_pressed_ios.svg new file mode 100644 index 0000000..3552b43 --- /dev/null +++ b/lab-nathan/frontend/src/assets/btn_google_dark_pressed_ios.svg @@ -0,0 +1,50 @@ + + + + btn_google_dark_pressed_ios + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lab-nathan/frontend/src/assets/btn_google_light_disabled_ios.svg b/lab-nathan/frontend/src/assets/btn_google_light_disabled_ios.svg new file mode 100644 index 0000000..b433d04 --- /dev/null +++ b/lab-nathan/frontend/src/assets/btn_google_light_disabled_ios.svg @@ -0,0 +1,24 @@ + + + + btn_google_light_disabled_ios + Created with Sketch. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lab-nathan/frontend/src/assets/btn_google_light_focus_ios.svg b/lab-nathan/frontend/src/assets/btn_google_light_focus_ios.svg new file mode 100644 index 0000000..1f3ee4f --- /dev/null +++ b/lab-nathan/frontend/src/assets/btn_google_light_focus_ios.svg @@ -0,0 +1,44 @@ + + + + btn_google_light_focus_ios + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lab-nathan/frontend/src/assets/btn_google_light_normal_ios.svg b/lab-nathan/frontend/src/assets/btn_google_light_normal_ios.svg new file mode 100644 index 0000000..032b6ac --- /dev/null +++ b/lab-nathan/frontend/src/assets/btn_google_light_normal_ios.svg @@ -0,0 +1,43 @@ + + + + btn_google_light_normal_ios + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lab-nathan/frontend/src/assets/btn_google_light_pressed_ios.svg b/lab-nathan/frontend/src/assets/btn_google_light_pressed_ios.svg new file mode 100644 index 0000000..ee593d7 --- /dev/null +++ b/lab-nathan/frontend/src/assets/btn_google_light_pressed_ios.svg @@ -0,0 +1,43 @@ + + + + btn_google_light_pressed_ios + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lab-nathan/frontend/src/assets/camera-icon.svg b/lab-nathan/frontend/src/assets/camera-icon.svg new file mode 100644 index 0000000..2538ac2 --- /dev/null +++ b/lab-nathan/frontend/src/assets/camera-icon.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + Camera + 2012-10-29T03:23:26 + + https://openclipart.org/detail/172916/camera-by-eternaltyro-172916 + + + eternaltyro + + + + + Camera + photo + + + + + + + + + + + diff --git a/lab-nathan/frontend/src/assets/noise.png b/lab-nathan/frontend/src/assets/noise.png new file mode 100644 index 0000000..dd37251 Binary files /dev/null and b/lab-nathan/frontend/src/assets/noise.png differ diff --git a/lab-nathan/frontend/src/components/app/_app.scss b/lab-nathan/frontend/src/components/app/_app.scss new file mode 100644 index 0000000..d8c3eb3 --- /dev/null +++ b/lab-nathan/frontend/src/components/app/_app.scss @@ -0,0 +1,5 @@ +.cfgram { + height: 100%; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/app/index.js b/lab-nathan/frontend/src/components/app/index.js new file mode 100644 index 0000000..98833c4 --- /dev/null +++ b/lab-nathan/frontend/src/components/app/index.js @@ -0,0 +1,30 @@ +import './_app.scss'; +import React from 'react'; +import {BrowserRouter, Route} from 'react-router-dom'; +import Landing from '../landing'; +import Dashboard from '../dashboard'; +import Settings from '../settings'; +import Header from '../header'; +import Footer from '../footer'; +import Content from '../content'; + + +class App extends React.Component { + render() { + return ( + +
+ + + + + + +
+
+ ); + } +} + +export default App; \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/avatar/_avatar.scss b/lab-nathan/frontend/src/components/avatar/_avatar.scss new file mode 100644 index 0000000..ecc8449 --- /dev/null +++ b/lab-nathan/frontend/src/components/avatar/_avatar.scss @@ -0,0 +1,8 @@ +@import '../../styles/lib/vars'; + +.avatar { + & img { + height: 48px; + margin-right: $gutter / 2; + } +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/avatar/index.js b/lab-nathan/frontend/src/components/avatar/index.js new file mode 100644 index 0000000..008de3b --- /dev/null +++ b/lab-nathan/frontend/src/components/avatar/index.js @@ -0,0 +1,8 @@ +import './_avatar.scss'; +import React from 'react' + +export default (props) => ( +
+ +
+) diff --git a/lab-nathan/frontend/src/components/content/_content.scss b/lab-nathan/frontend/src/components/content/_content.scss new file mode 100644 index 0000000..bd118e4 --- /dev/null +++ b/lab-nathan/frontend/src/components/content/_content.scss @@ -0,0 +1,5 @@ +main { + flex: 1; + text-align: center; + background: url('../../assets/noise.png'); +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/content/index.js b/lab-nathan/frontend/src/components/content/index.js new file mode 100644 index 0000000..7150ea2 --- /dev/null +++ b/lab-nathan/frontend/src/components/content/index.js @@ -0,0 +1,14 @@ +import './_content.scss'; +import React from 'react'; + +class Content extends React.Component { + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +export default Content; \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/dashboard/_dashboard.scss b/lab-nathan/frontend/src/components/dashboard/_dashboard.scss new file mode 100644 index 0000000..c9db236 --- /dev/null +++ b/lab-nathan/frontend/src/components/dashboard/_dashboard.scss @@ -0,0 +1,5 @@ +.dashboard { + height: 100%; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/dashboard/index.js b/lab-nathan/frontend/src/components/dashboard/index.js new file mode 100644 index 0000000..8cf8c86 --- /dev/null +++ b/lab-nathan/frontend/src/components/dashboard/index.js @@ -0,0 +1,51 @@ +import './_dashboard.scss'; +import React from 'react' +import {connect} from 'react-redux' +import * as util from '../../lib/utilities.js' +import * as photoActions from '../../actions/photo-actions.js' + +import PhotoForm from '../photo-form' +import PhotoItem from '../photo-item' + +class DashboardContainer extends React.Component { + constructor(props){ + super(props); + } + + componentDidMount(){ + this.props.photosFetch() + .catch(util.logError); + } + + render(){ + return ( +
+ { + return this.props.photoCreate(photo) + .catch(console.error) + }} + /> + {this.props.photos.map(photo => + + )} +
+ ) + } +} + +let mapStateToProps = (state) => ({ + profile: state.profile, + photos: state.photos, +}) + +let mapDispatchToProps = (dispatch) => ({ + photoCreate: (photo) => dispatch(photoActions.userPhotoCreateRequest(photo)), + photosFetch: (photos) => dispatch(photoActions.userPhotosFetchRequest()), +}) + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(DashboardContainer) diff --git a/lab-nathan/frontend/src/components/footer/_footer.scss b/lab-nathan/frontend/src/components/footer/_footer.scss new file mode 100644 index 0000000..5a7221a --- /dev/null +++ b/lab-nathan/frontend/src/components/footer/_footer.scss @@ -0,0 +1,7 @@ +@import '../../styles/lib/vars'; + +footer { + min-height: 50px; + background: $bookend-background; + box-shadow: $bookend-shadow; +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/footer/index.js b/lab-nathan/frontend/src/components/footer/index.js new file mode 100644 index 0000000..5bd592d --- /dev/null +++ b/lab-nathan/frontend/src/components/footer/index.js @@ -0,0 +1,14 @@ +import './_footer.scss'; +import React from 'react'; + +class Footer extends React.Component { + render() { + return ( + + ); + } +} + +export default Footer; \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/google-oauth/_google-oauth.scss b/lab-nathan/frontend/src/components/google-oauth/_google-oauth.scss new file mode 100644 index 0000000..cedbd62 --- /dev/null +++ b/lab-nathan/frontend/src/components/google-oauth/_google-oauth.scss @@ -0,0 +1,31 @@ +.google-oauth { + background-color: #4285F4; + color: white; + font-family: 'Roboto', sans-serif; + font-size: 14px; + margin: 0 auto; + width: 175px; + height: 40px; + border-radius: 3px; + display: block; + text-align: left; + + + & div { + display: inline-block; + vertical-align: middle; + height: 40px; + width: 40px; + } + + & svg { + height: 40px; + width: 40px; + } + + & p { + display: inline-block; + vertical-align: middle; + padding-left: 5px; + } +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/google-oauth/index.js b/lab-nathan/frontend/src/components/google-oauth/index.js new file mode 100644 index 0000000..45f1a9b --- /dev/null +++ b/lab-nathan/frontend/src/components/google-oauth/index.js @@ -0,0 +1,27 @@ +import './_google-oauth.scss'; +import React from 'react'; +import superagent from 'superagent'; + +class GoogleOAuth extends React.Component { + render() { + let data = require(`../../assets/btn_google_dark_normal_ios.svg`); + let innerHtml = {__html: data}; + + let AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'; + let clientIDQuery = `client_id=${__GOOGLE_CLIENT_ID__}` + let responseTypeQuery = 'response_type=code'; + let scopeQuery = 'scope=openid%20profile%20email'; + let promptQuery = 'prompt=consent'; + let redirectURIQuery = 'redirect_uri=http://localhost:3000/oauth/google/code'; + let formattedURI = `${AUTH_URL}?${clientIDQuery}&${responseTypeQuery}&${scopeQuery}&${promptQuery}&${redirectURIQuery}`; + + return ( + +
+

Sign in with Google

+
+ ); + } +} + +export default GoogleOAuth; \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/header/_header.scss b/lab-nathan/frontend/src/components/header/_header.scss new file mode 100644 index 0000000..8d52b4f --- /dev/null +++ b/lab-nathan/frontend/src/components/header/_header.scss @@ -0,0 +1,14 @@ +@import '../../styles/lib/vars'; + +header { + min-height: 75px; + background: $bookend-background; + box-shadow: $bookend-shadow; + + display: flex; + align-items: center; +} + +#logout-button { + margin-right: $gutter / 2; +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/header/index.js b/lab-nathan/frontend/src/components/header/index.js new file mode 100644 index 0000000..738320d --- /dev/null +++ b/lab-nathan/frontend/src/components/header/index.js @@ -0,0 +1,85 @@ +import './_header.scss'; +import React from 'react'; +import {Link} from 'react-router-dom'; +import Logo from '../logo'; +import Menu from '../menu'; +import MenuItem from '../menu-item'; +import Avatar from '../avatar'; +import * as util from '../../lib/utilities.js'; +import {tokenSet, logout} from '../../actions/token-actions.js'; +import {profileFetchRequest} from '../../actions/profile-actions.js'; +import {connect} from 'react-redux'; + +class Header extends React.Component { + constructor(props){ + super(props) + this.validateRoute = this.validateRoute.bind(this) + this.handleLogout = this.handleLogout.bind(this) + } + + componentDidMount(){ + this.validateRoute(this.props) + } + + validateRoute(props){ + let {match, history} = props + let token = util.readCookie('X-Sluggram-Token') + + if(!token){ + return history.replace('/welcome/signup') + } + + this.props.tokenSet(token) + this.props.profileFetch() + .catch(() => { + console.log('PROFILE FETCH ERROR: user does not have a userProfile') + if(!match.url.startsWith('/settings')){ + return history.replace('/settings') + } + }) + } + + handleLogout(){ + this.props.logout() + this.props.history.push('/welcome/login') + } + + render() { + return ( +
+ + {util.renderIf(!this.props.loggedIn, + + sign up + log in + + )} + {util.renderIf(this.props.loggedIn, + + dashboard + settings + + )} + {util.renderIf(this.props.profile, + + )} + {util.renderIf(this.props.loggedIn, + + )} +
+ ); + } +} + +let mapStateToProps = (state) => ({ + loggedIn: !!state.token, + profile: state.profile +}); + +let mapDispatchToProps = (dispatch) => ({ + tokenSet: (token) => dispatch(tokenSet(token)), + logout: () => dispatch(logout()), + profileFetch: () => dispatch(profileFetchRequest()) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Header); \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/landing/_landing.scss b/lab-nathan/frontend/src/components/landing/_landing.scss new file mode 100644 index 0000000..3dbe16b --- /dev/null +++ b/lab-nathan/frontend/src/components/landing/_landing.scss @@ -0,0 +1,6 @@ +@import '../../styles/lib/vars'; + +.landing { + height: 100%; + display: flex; +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/landing/index.js b/lab-nathan/frontend/src/components/landing/index.js new file mode 100644 index 0000000..cba8eb5 --- /dev/null +++ b/lab-nathan/frontend/src/components/landing/index.js @@ -0,0 +1,29 @@ +import './_landing.scss'; +import React from 'react'; +import {connect} from 'react-redux'; +import {signupRequest, loginRequest} from '../../actions/token-actions.js'; +import Login from '../login'; + +class Landing extends React.Component { + render() { + let {params} = this.props.match; + let handleComplete = params.auth === 'login' + ? this.props.login + : this.props.signup; + + return ( +
+ +
+ ); + } +} + +let mapDispatchToProps = (dispatch) =>({ + signup: (user) => dispatch(signupRequest(user)), + login: (user) => dispatch(loginRequest(user)) +}) + +export default connect(null, mapDispatchToProps)(Landing); \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/login/_login.scss b/lab-nathan/frontend/src/components/login/_login.scss new file mode 100644 index 0000000..32d6b85 --- /dev/null +++ b/lab-nathan/frontend/src/components/login/_login.scss @@ -0,0 +1,34 @@ +@import '../../styles/lib/vars'; + +.user-form { + background: url('../../assets/noise.png') #dadada; + padding: $gutter * 1.5 $gutter; + margin: auto; + border-radius: 5px; + box-shadow: 3px 3px 10px -4px rgba(0, 0, 0, 0.75); + + & input { + display: block; + margin: $gutter / 3 auto; + padding: $input-padding; + font-size: $input-font-size; + border-radius: $input-border-radius; + border: $input-border; + } + + & button { + margin-top: $gutter / 2; + padding: $input-padding * 3 / 4 $input-padding * 2; + font-size: 1rem; + border-radius: 3px; + border: none; + background: url('../../assets/noise.png') #666; + color: white; + } + + & .separator { + height: 1px; + background-color: #bbb; + margin: $gutter 0; + } +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/login/index.js b/lab-nathan/frontend/src/components/login/index.js new file mode 100644 index 0000000..d5a9cf2 --- /dev/null +++ b/lab-nathan/frontend/src/components/login/index.js @@ -0,0 +1,94 @@ +import './_login.scss'; +import React from 'react'; +import * as utilities from '../../lib/utilities.js'; +import GoogleOAuth from '../google-oauth'; + +class Login extends React.Component { + constructor(props) { + super(props); + this.state = { + username: '', + email: '', + password: '', + usernameError: null, + passwordError: null, + emailError: null, + error: null + } + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleChange(e) { + let {name, value} = e.target; + this.setState({ + [name]: value, + usernameError: name === 'username' && !value ? 'username required' : null, + emailError: name === 'email' && !value ? 'email required' : null, + passwordError: name === 'password' && !value ? 'password required' : null + }); + } + + handleSubmit(e) { + e.preventDefault(); + this.props.onComplete(this.state) + .then(() => this.setState({ username: '', email: '', password: '' })) + .catch(error => { + console.error(error); + this.setState({error}); + }); + } + + render() { + return ( +
+ + {utilities.renderIf(this.props.auth === 'signup', + + )} + + {utilities.renderIf(this.state.usernameError, + + {this.state.usernameError} + + )} + + + + {utilities.renderIf(this.state.passwordError, + + {this.state.passwordError} + + )} + + + + + +
+ + + + ); + } +} + +export default Login; diff --git a/lab-nathan/frontend/src/components/logo/_logo.scss b/lab-nathan/frontend/src/components/logo/_logo.scss new file mode 100644 index 0000000..030910f --- /dev/null +++ b/lab-nathan/frontend/src/components/logo/_logo.scss @@ -0,0 +1,27 @@ +@import '../../styles/lib/vars'; + +#logo { + margin: 0 $gutter; + flex: 1; +} + +#logo-text { + font-size: $logo-size; + font-weight: bold; + font-family: 'Playfair Display', serif; + color: $primary-dark; + display: inline-block; + vertical-align: middle; +} + +#logo-image { + height: 48px; + width: 48px; + display: inline-block; + vertical-align: middle; + margin-right: $gutter / 4; +} + +#path3869, #path3853 { + fill: $primary-dark; +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/logo/index.js b/lab-nathan/frontend/src/components/logo/index.js new file mode 100644 index 0000000..ec5bbae --- /dev/null +++ b/lab-nathan/frontend/src/components/logo/index.js @@ -0,0 +1,18 @@ +import './_logo.scss'; +import React from 'react'; + +class Logo extends React.Component { + render() { + let data = require(`../../assets/camera-icon.svg`) + let innerHtml = {__html: data} + + return ( + + ); + } +} + +export default Logo; \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/menu-item/_menu-item.scss b/lab-nathan/frontend/src/components/menu-item/_menu-item.scss new file mode 100644 index 0000000..2010bb6 --- /dev/null +++ b/lab-nathan/frontend/src/components/menu-item/_menu-item.scss @@ -0,0 +1,14 @@ +@import '../../styles/lib/vars'; + +.menu-item { + display: inline; + margin-left: $gutter / 2; + font-size: 1.125rem; + color: $primary-dark; + font-family: $font-family; + + & a { + color: inherit; + text-decoration: none; + } +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/menu-item/index.js b/lab-nathan/frontend/src/components/menu-item/index.js new file mode 100644 index 0000000..c90460a --- /dev/null +++ b/lab-nathan/frontend/src/components/menu-item/index.js @@ -0,0 +1,12 @@ +import './_menu-item.scss'; +import React from 'react'; + +class MenuItem extends React.Component { + render() { + return ( +
  • {this.props.children}
  • + ); + } +} + +export default MenuItem; \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/menu/_menu.scss b/lab-nathan/frontend/src/components/menu/_menu.scss new file mode 100644 index 0000000..51c2273 --- /dev/null +++ b/lab-nathan/frontend/src/components/menu/_menu.scss @@ -0,0 +1,6 @@ +@import '../../styles/lib/vars'; + +.menu { + margin-right: $gutter; + font-family: $font-family; +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/menu/index.js b/lab-nathan/frontend/src/components/menu/index.js new file mode 100644 index 0000000..d13ed2b --- /dev/null +++ b/lab-nathan/frontend/src/components/menu/index.js @@ -0,0 +1,14 @@ +import './_menu.scss'; +import React from 'react'; + +class Menu extends React.Component { + render() { + return ( + + ); + } +} + +export default Menu; \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/photo-form/_photo-form.scss b/lab-nathan/frontend/src/components/photo-form/_photo-form.scss new file mode 100644 index 0000000..8015689 --- /dev/null +++ b/lab-nathan/frontend/src/components/photo-form/_photo-form.scss @@ -0,0 +1,41 @@ +@import '../../styles/lib/vars'; + +.photo-form { + background: url('../../assets/noise.png') #dadada; + padding: $gutter; + margin: auto; + border-radius: 5px; + box-shadow: 3px 3px 10px -4px rgba(0, 0, 0, 0.75); + + & img { + max-height: 300px; + } + + & input { + display: block; + margin: $gutter / 3 auto; + padding: $input-padding; + font-size: $input-font-size; + border-radius: $input-border-radius; + } + + & button { + margin-top: $gutter / 2; + padding: $input-padding * 3 / 4 $input-padding * 2; + font-size: 1rem; + border-radius: 3px; + border: none; + background: url('../../assets/noise.png') #666; + color: white; + } + + & textarea { + display: block; + width: 100%; + min-height: 100px; + font-size: inherit; + font-family: helvetica; + padding: 10px; + box-sizing: border-box; + } +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/photo-form/index.js b/lab-nathan/frontend/src/components/photo-form/index.js new file mode 100644 index 0000000..e69537a --- /dev/null +++ b/lab-nathan/frontend/src/components/photo-form/index.js @@ -0,0 +1,70 @@ +import './_photo-form.scss'; +import React from 'react'; +import * as util from '../../lib/utilities.js' + +class PhotoForm extends React.Component { + constructor(props){ + super(props) + this.state = props.photo + ? props.photo + : {description: '' , preview: '', photo: null} + + this.handleChange = this.handleChange.bind(this) + this.handleSubmit = this.handleSubmit.bind(this) + } + + handleChange(e){ + let {name} = e.target + if(name == 'description'){ + this.setState({description: e.target.value}) + } + + if(name == 'photo'){ + let {files} = e.target + let photo = files[0] + this.setState({photo}) + util.photoToDataURL(photo) + .then(preview => this.setState({preview})) + .catch(console.error) + } + } + + handleSubmit(e){ + e.preventDefault() + return this.props.onComplete(this.state) + .then(() => { + if(!this.props.profile){ + this.setState({description: '' , preview: '', photo: null}) + } + }) + } + + render(){ + return ( +
    + + {util.renderIf(this.state.preview, + )} + + + + + + +
    + ) + } +} + +export default ProfileForm; \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/profile/index.js b/lab-nathan/frontend/src/components/profile/index.js new file mode 100644 index 0000000..cc93ed6 --- /dev/null +++ b/lab-nathan/frontend/src/components/profile/index.js @@ -0,0 +1,7 @@ +import React from 'react' + +export default (props) => ( +
    + +
    +) \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/settings/_settings.scss b/lab-nathan/frontend/src/components/settings/_settings.scss new file mode 100644 index 0000000..07e3b58 --- /dev/null +++ b/lab-nathan/frontend/src/components/settings/_settings.scss @@ -0,0 +1,7 @@ +@import '../../styles/lib/vars'; + +.settings-container { + height: 100%; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/components/settings/index.js b/lab-nathan/frontend/src/components/settings/index.js new file mode 100644 index 0000000..e07c401 --- /dev/null +++ b/lab-nathan/frontend/src/components/settings/index.js @@ -0,0 +1,56 @@ +import './_settings.scss'; +import React from 'react'; +import {connect} from 'react-redux'; +import ProfileForm from '../profile-form'; +import {profileCreateRequest, profileUpdateRequest} from '../../actions/profile-actions.js'; + +class SettingsContainer extends React.Component { + constructor(props) { + super(props); + this.handleProfileCreate = this.handleProfileCreate.bind(this); + this.handleProfileUpdate = this.handleProfileUpdate.bind(this); + } + + handleProfileCreate(profile) { + return this.props.profileCreate(profile) + .then(response => { + console.log(response); + }) + .catch(console.error); + } + + handleProfileUpdate(profile) { + return this.props.profileUpdate(profile) + .catch(console.error) + } + + render() { + let handleComplete = this.props.profile + ? this.handleProfileUpdate + : this.handleProfileCreate; + + let buttonText = this.props.profile + ? 'update profile' + : 'create profile'; + + return ( +
    + +
    + ); + } +} + +let mapStateToProps = (state) => ({ + profile: state.profile +}) + +let mapDispatchToProps = (dispatch) => ({ + profileCreate: (profile) => dispatch(profileCreateRequest(profile)), + profileUpdate: (profile) => dispatch(profileUpdateRequest(profile)) +}) + +export default connect(mapStateToProps, mapDispatchToProps)(SettingsContainer); \ No newline at end of file diff --git a/lab-nathan/frontend/src/index.html b/lab-nathan/frontend/src/index.html new file mode 100644 index 0000000..44d42a5 --- /dev/null +++ b/lab-nathan/frontend/src/index.html @@ -0,0 +1,9 @@ + + + + CF Gram + + +
    + + \ No newline at end of file diff --git a/lab-nathan/frontend/src/lib/create-store.js b/lab-nathan/frontend/src/lib/create-store.js new file mode 100644 index 0000000..b52ae88 --- /dev/null +++ b/lab-nathan/frontend/src/lib/create-store.js @@ -0,0 +1,6 @@ +import {createStore, applyMiddleware} from 'redux'; +import reducers from '../reducers'; +import thunk from './thunk.js'; +import reporter from './reporter.js'; + +export default () => createStore(reducers, applyMiddleware(thunk, reporter)); \ No newline at end of file diff --git a/lab-nathan/frontend/src/lib/reporter.js b/lab-nathan/frontend/src/lib/reporter.js new file mode 100644 index 0000000..7350185 --- /dev/null +++ b/lab-nathan/frontend/src/lib/reporter.js @@ -0,0 +1,14 @@ +export default store => next => action => { + console.log('__ACTION__', action); + + try { + let result = next(action); + console.log('__STATE__', store.getState()); + return result; + } + catch(error) { + error.action = action; + console.error('__ERROR__', error); + return error; + } +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/lib/thunk.js b/lab-nathan/frontend/src/lib/thunk.js new file mode 100644 index 0000000..c3a44ca --- /dev/null +++ b/lab-nathan/frontend/src/lib/thunk.js @@ -0,0 +1,6 @@ +export default store => next => action => { + if (typeof action === 'function') + return action(store.dispatch, store.getState); + else + return next(action); +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/lib/utilities.js b/lab-nathan/frontend/src/lib/utilities.js new file mode 100644 index 0000000..025acc9 --- /dev/null +++ b/lab-nathan/frontend/src/lib/utilities.js @@ -0,0 +1,73 @@ +export const log = (...args) => +__DEBUG__ ? console.log(...args) : undefined; + +export const logError = (...args) => +__DEBUG__ ? console.error(...args) : undefined; + +export const renderIf = (test, component) => test ? component : undefined; + +export const classToggler = (options) => +Object.keys(options).filter(key => Boolean(options)).join(' '); + +export const map = (list, ...args) => +Array.prototype.map.apply(list, args); + +export const filter = (list, ...args) => +Array.prototype.filter.apply(list, args); + +export const reduce = (list, ...args) => +Array.prototype.reduce.apply(list, args); + +export const photoToDataURL = (file) => { + return new Promise((resolve, reject) => { + let reader = new FileReader(); + + reader.addEventListener('load', () => { + resolve(reader.result); + }); + + reader.addEventListener('error', () => { + reject(reader.error); + }); + + if (file) { + return reader.readAsDataURL(file); + } + + return reject(new Error('USAGE ERROR: requires file')); + }); +} + +// https://stackoverflow.com/questions/14573223/set-cookie-and-get-cookie-with-javascript +export const readCookie = (name) => { + var nameEquals = name + '='; + var attributes = document.cookie.split(';'); + + for (var i = 0; i < attributes.length; i++) { + var attribute = attributes[i]; + + while (attribute.charAt(0) == ' ') { + attribute = attribute.substring(1, attribute.length); + } + + if (attribute.indexOf(nameEquals) == 0) { + return attribute.substring(nameEquals.length, attribute.length); + } + } + + return null; +} + +export const createCookie = (name,value,days) => { + if (days) { + var date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + var expires = "; expires="+date.toGMTString(); + } + else var expires = ""; + document.cookie = name+"="+value+expires+"; path=/"; +} + +export const deleteCookie = (name) => { + createCookie(name,"",-1); +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/main.js b/lab-nathan/frontend/src/main.js new file mode 100644 index 0000000..811dacf --- /dev/null +++ b/lab-nathan/frontend/src/main.js @@ -0,0 +1,16 @@ +import './styles/main.scss'; +import React from 'react'; +import ReactDom from 'react-dom'; +import {Provider} from 'react-redux'; +import App from './components/app'; +import createStore from './lib/create-store.js'; + +let AppContainer = () => { + return ( + + + + ) +} + +ReactDom.render(, document.getElementById('root')); \ No newline at end of file diff --git a/lab-nathan/frontend/src/reducers/index.js b/lab-nathan/frontend/src/reducers/index.js new file mode 100644 index 0000000..6d09997 --- /dev/null +++ b/lab-nathan/frontend/src/reducers/index.js @@ -0,0 +1,10 @@ +import {combineReducers} from 'redux'; +import tokenReducer from './token-reducer.js'; +import profileReducer from './profile-reducer.js'; +import photosReducer from './photos-reducer.js'; + +export default combineReducers({ + token: tokenReducer, + profile: profileReducer, + photos: photosReducer +}); \ No newline at end of file diff --git a/lab-nathan/frontend/src/reducers/photos-reducer.js b/lab-nathan/frontend/src/reducers/photos-reducer.js new file mode 100644 index 0000000..8872dd5 --- /dev/null +++ b/lab-nathan/frontend/src/reducers/photos-reducer.js @@ -0,0 +1,17 @@ +export default (state=[], action) => { + let {type, payload} = action + switch(type){ + case 'USER_PHOTOS_SET': + return payload + case 'USER_PHOTO_CREATE': + return [payload, ...state] + case 'USER_PHOTO_UPDATE': + return state.map(item => item._id === payload._id ? payload : item) + case 'USER_PHOTO_DELETE': + return state.filter(item => item._id !== payload._id) + case 'LOGOUT': + return [] + default: + return state + } +} diff --git a/lab-nathan/frontend/src/reducers/profile-reducer.js b/lab-nathan/frontend/src/reducers/profile-reducer.js new file mode 100644 index 0000000..96cea91 --- /dev/null +++ b/lab-nathan/frontend/src/reducers/profile-reducer.js @@ -0,0 +1,24 @@ +let validateProfileCreate = (profile) => { + if(!profile.avatar || !profile.bio || !profile._id + || !profile.owner || !profile.username || !profile.email){ + throw new Error('VALIDATION ERROR: profile requires avatar and bio') + } +} + +export default (state=null, action) => { + let {type, payload} = action + switch(type){ + case 'PROFILE_CREATE': + validateProfileCreate(payload) + return payload + case 'PROFILE_UPDATE': + if(!state) + throw new Error('USAGE ERROR: can not update when profile is null') + validateProfileCreate(payload) + return {...state, ...payload} + case 'LOGOUT': + return null + default: + return state + } +} diff --git a/lab-nathan/frontend/src/reducers/token-reducer.js b/lab-nathan/frontend/src/reducers/token-reducer.js new file mode 100644 index 0000000..7e41a61 --- /dev/null +++ b/lab-nathan/frontend/src/reducers/token-reducer.js @@ -0,0 +1,12 @@ +export default (state = null, action) => { + let {type, payload} = action; + switch(type) { + case 'TOKEN_SET': + return payload + case 'TOKEN_DELETE': + case 'LOGOUT': + return null + default: + return state + } +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/styles/base/_base.scss b/lab-nathan/frontend/src/styles/base/_base.scss new file mode 100644 index 0000000..47a749b --- /dev/null +++ b/lab-nathan/frontend/src/styles/base/_base.scss @@ -0,0 +1,3 @@ +html, body, #root { + height: 100%; +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/styles/base/_reset.scss b/lab-nathan/frontend/src/styles/base/_reset.scss new file mode 100644 index 0000000..af94440 --- /dev/null +++ b/lab-nathan/frontend/src/styles/base/_reset.scss @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/lab-nathan/frontend/src/styles/layout/_content.scss b/lab-nathan/frontend/src/styles/layout/_content.scss new file mode 100644 index 0000000..e69de29 diff --git a/lab-nathan/frontend/src/styles/layout/_footer.scss b/lab-nathan/frontend/src/styles/layout/_footer.scss new file mode 100644 index 0000000..e69de29 diff --git a/lab-nathan/frontend/src/styles/layout/_header.scss b/lab-nathan/frontend/src/styles/layout/_header.scss new file mode 100644 index 0000000..e69de29 diff --git a/lab-nathan/frontend/src/styles/lib/_vars.scss b/lab-nathan/frontend/src/styles/lib/_vars.scss new file mode 100644 index 0000000..2fe7b91 --- /dev/null +++ b/lab-nathan/frontend/src/styles/lib/_vars.scss @@ -0,0 +1,11 @@ +$primary: hsla(140, 52%, 55%, 1); +$primary-dark: hsla(140, 52%, 25%, 1);; +$bookend-background: url('../../assets/noise.png') hsl(140, 52%, 55%); +$bookend-shadow: 0px 0px 5px 2px rgba(0,0,0,0.5); +$logo-size: 2.25rem; +$gutter: 30px; +$font-family: 'helvetica', sans-serif; +$input-padding: 10px; +$input-font-size: 1.125rem; +$input-border-radius: 3px; +$input-border: 1px solid #ddd; \ No newline at end of file diff --git a/lab-nathan/frontend/src/styles/main.scss b/lab-nathan/frontend/src/styles/main.scss new file mode 100644 index 0000000..bcea48b --- /dev/null +++ b/lab-nathan/frontend/src/styles/main.scss @@ -0,0 +1,5 @@ +@import url('https://fonts.googleapis.com/css?family=Playfair+Display'); +@import url('https://fonts.googleapis.com/css?family=Roboto'); +@import "./lib/vars"; +@import "./base/reset"; +@import "./base/base"; \ No newline at end of file diff --git a/lab-nathan/frontend/webpack.config.js b/lab-nathan/frontend/webpack.config.js new file mode 100644 index 0000000..78950d4 --- /dev/null +++ b/lab-nathan/frontend/webpack.config.js @@ -0,0 +1,97 @@ +'use strict'; + +require('dotenv').config(); + +const production = process.env.NODE_ENV === 'production'; + +const { DefinePlugin, EnvironmentPlugin } = require('webpack'); +const HtmlPlugin = require('html-webpack-plugin'); +const CleanPlugin = require('clean-webpack-plugin'); +const UglifyPlugin = require('uglifyjs-webpack-plugin'); +const ExtractPlugin = require('extract-text-webpack-plugin'); + +console.log(process.env.API_URL); + +let plugins = [ + new EnvironmentPlugin(['NODE_ENV']), + new ExtractPlugin('bundle-[hash].css'), + new HtmlPlugin({ template: `${__dirname}/src/index.html` }), + new DefinePlugin({ + '__DEBUG__': JSON.stringify('development'), + '__API_URL__': JSON.stringify(process.env.API_URL), + '__GOOGLE_CLIENT_ID__': JSON.stringify(process.env.GOOGLE_CLIENT_ID) + }) +]; + +if (production) { + plugins = plugins.concat([ new CleanPlugin(), new UglifyPlugin() ]); +} + +module.exports = { + plugins, + entry: `${__dirname}/src/main.js`, + devServer: { + historyApiFallback: true + }, + devtool: production ? undefined : 'eval', + output: { + path: `${__dirname}/build`, + filename: 'bundle-[hash].js', + publicPath: process.env.CDN_URL + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }, + { + test: /\.scss$/, + loader: ExtractPlugin.extract(['css-loader', 'sass-loader']) + }, + { + test: /\.(svg)$/, + loader: 'raw-loader', + }, + { + test: /\.(woff|woff2|ttf|eot|glyph|\.svg)$/, + exclude: /\camera-icon.svg|btn_google_dark_normal_ios.svg$/, + use: [ + { + loader: 'url-loader', + options: { + limit: 10000, + name: 'font/[name].[ext]' + } + } + ] + }, + { + test: /\.(jpg|jpeg|gif|png|tiff|svg)$/, + exclude: /\glyph.svg|camera-icon.svg|btn_google_dark_normal_ios.svg$/, + use: [ + { + loader: 'url-loader', + options: { + limit: 6000, + name: 'image/[name].[ext]' + }, + } + ] + }, + { + test: /\.(mp3|aac|aiff|wav|flac|m4a|mp4|ogg|ape)$/, + exclude: /\glyph.svg|camera-icon.svg|btn_google_dark_normal_ios.svg$/, + use: [ + { + loader: 'file-loader', + options: { + name: 'audio/[name].[ext]' + } + } + ] + } + ] + } +};