From 427aa2d78646ad72dc0d3004c00e23b568415d8f Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Wed, 29 Mar 2017 13:24:09 +0200 Subject: [PATCH 001/143] django updated --- app/l8pr/api.py | 2 ++ app/settings.py | 2 +- requirements.txt | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/l8pr/api.py b/app/l8pr/api.py index 6b77e93..dc7f60d 100644 --- a/app/l8pr/api.py +++ b/app/l8pr/api.py @@ -16,6 +16,7 @@ class ItemSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) class Meta: + fields = '__all__' model = Item @@ -27,6 +28,7 @@ class Meta: class ProfileSerializer(serializers.ModelSerializer): class Meta: + fields = '__all__' model = Profile diff --git a/app/settings.py b/app/settings.py index 7709e3b..5d3b61a 100644 --- a/app/settings.py +++ b/app/settings.py @@ -169,7 +169,7 @@ ), 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 'DEFAULT_FILTER_BACKENDS': ( - 'rest_framework.filters.DjangoFilterBackend', + 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.OrderingFilter' ) } diff --git a/requirements.txt b/requirements.txt index bbc0661..1e6aafa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -Django==1.9.3 +Django==1.10.5 django-appconf==1.0.1 django-compressor==2.0 -django-filter==0.12.0 -djangorestframework==3.3.2 +django-filter==1.0.1 +djangorestframework==3.5.4 Markdown==2.6.5 rcssmin==1.0.6 rjsmin==1.0.12 @@ -10,7 +10,7 @@ six==1.10.0 wheel==0.24.0 google-api-python-client==1.5.0 soundcloud==0.5.0 -django-rest-framework-social-oauth2==1.0.4 +django-rest-framework-social-oauth2==1.0.5 whitenoise==2.0.6 git+https://github.com/django-haystack/django-haystack.git@cc0974d53de637c16468b6779d486794ac9caaf5 elasticsearch==2.3.0 From 84155c7a647d3028adcc47e674d666cfba2fd63e Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Wed, 29 Mar 2017 16:22:59 +0200 Subject: [PATCH 002/143] react --- .babelrc | 16 + .bootstraprc | 117 +++ .gitignore | 1 + app/l8pr/static/actions/auth.js | 92 ++ app/l8pr/static/actions/browser.js | 54 ++ app/l8pr/static/actions/data.js | 58 ++ app/l8pr/static/actions/player.js | 51 + app/l8pr/static/api.service.js | 139 --- app/l8pr/static/app.js | 60 ++ app/l8pr/static/components/Loop/index.js | 43 + app/l8pr/static/components/Loop/style.scss | 21 + app/l8pr/static/components/LoopItem/index.js | 20 + .../static/components/LoopItem/style.scss | 2 + app/l8pr/static/components/NavPlayer/index.js | 36 + .../static/components/NavPlayer/style.scss | 3 + app/l8pr/static/components/Screen/index.js | 17 + app/l8pr/static/components/Screen/style.scss | 7 + app/l8pr/static/components/index.js | 3 + app/l8pr/static/constants/index.js | 21 + .../static/containers/Controller/index.js | 14 + app/l8pr/static/containers/Home/index.js | 53 + app/l8pr/static/containers/Home/style.scss | 8 + app/l8pr/static/containers/Login/index.js | 134 +++ app/l8pr/static/containers/NotFound/index.js | 13 + app/l8pr/static/containers/Protected/index.js | 83 ++ app/l8pr/static/containers/Root/DevTools.js | 23 + app/l8pr/static/containers/Root/Root.dev.js | 29 + app/l8pr/static/containers/Root/Root.js | 5 + app/l8pr/static/containers/Root/Root.prod.js | 27 + app/l8pr/static/containers/Strip/index.js | 29 + app/l8pr/static/containers/Strip/style.scss | 6 + .../static/containers/StripHeader/index.js | 36 + .../static/containers/StripHeader/style.scss | 12 + app/l8pr/static/containers/index.js | 7 + app/l8pr/static/index.html | 31 + app/l8pr/static/index.js | 33 + app/l8pr/static/login/module.js | 92 -- app/l8pr/static/login/template.html | 72 -- app/l8pr/static/main.html | 116 --- app/l8pr/static/main.js | 328 ------- app/l8pr/static/main.less | 622 ------------ app/l8pr/static/player/giphy.js | 58 -- app/l8pr/static/player/player.ctrl.js | 146 --- app/l8pr/static/player/player.service.js | 266 ----- app/l8pr/static/player/soundcloud/module.js | 110 --- app/l8pr/static/player/vimeo/module.js | 76 -- app/l8pr/static/player/webtorrent/module.js | 100 -- app/l8pr/static/player/webtorrent/style.less | 17 - app/l8pr/static/player/youtube/module.js | 86 -- app/l8pr/static/reducers/auth.js | 51 + app/l8pr/static/reducers/browser.js | 21 + app/l8pr/static/reducers/data.js | 21 + app/l8pr/static/reducers/index.js | 14 + app/l8pr/static/reducers/player.js | 35 + app/l8pr/static/routes.js | 15 + app/l8pr/static/selectors.js | 50 + app/l8pr/static/store/configureStore.dev.js | 36 + app/l8pr/static/store/configureStore.js | 5 + app/l8pr/static/store/configureStore.prod.js | 18 + app/l8pr/static/strip/add-to-shows/module.js | 90 -- .../static/strip/add-to-shows/template.html | 44 - app/l8pr/static/strip/feed/module.js | 51 - app/l8pr/static/strip/feed/style.less | 29 - app/l8pr/static/strip/feed/template.html | 4 - app/l8pr/static/strip/header/module.js | 57 -- app/l8pr/static/strip/header/style.less | 82 -- app/l8pr/static/strip/header/template.html | 38 - app/l8pr/static/strip/help/module.js | 34 - app/l8pr/static/strip/help/style.less | 112 --- app/l8pr/static/strip/help/template.html | 84 -- app/l8pr/static/strip/loop/module.js | 135 --- app/l8pr/static/strip/loop/style.less | 131 --- app/l8pr/static/strip/loop/template.html | 32 - app/l8pr/static/strip/module.js | 130 --- .../static/strip/reset-password/module.js | 34 - .../static/strip/reset-password/template.html | 25 - .../static/strip/search-results/module.js | 55 -- .../static/strip/search-results/style.less | 18 - .../static/strip/search-results/template.html | 80 -- app/l8pr/static/strip/show-config/module.js | 46 - .../static/strip/show-config/template.html | 43 - app/l8pr/static/strip/show/module.js | 48 - app/l8pr/static/strip/show/style.less | 21 - app/l8pr/static/strip/show/template.html | 50 - app/l8pr/static/strip/style.less | 110 --- app/l8pr/static/styles/config/_colors.scss | 16 + app/l8pr/static/styles/config/_fonts.scss | 0 app/l8pr/static/styles/config/_reset.scss | 51 + .../static/styles/config/_typography.scss | 61 ++ app/l8pr/static/styles/config/_variables.scss | 3 + app/l8pr/static/styles/font-awesome.config.js | 18 + .../static/styles/font-awesome.config.less | 7 + .../static/styles/font-awesome.config.prod.js | 9 + app/l8pr/static/styles/main.scss | 48 + app/l8pr/static/styles/theme/_base.scss | 6 + app/l8pr/static/styles/theme/_footer.scss | 0 app/l8pr/static/styles/theme/_login.scss | 7 + app/l8pr/static/styles/theme/_navbar.scss | 7 + app/l8pr/static/styles/utils/_margins.scss | 37 + app/l8pr/static/utils/config.js | 5 + app/l8pr/static/utils/index.js | 20 + .../static/utils/requireAuthentication.js | 51 + app/l8pr/static/variables.less | 910 ------------------ app/l8pr/templates/index.html | 90 -- app/l8pr/views.py | 12 + app/settings.py | 18 +- app/urls.py | 11 +- package.json | 115 ++- protractor.js | 10 - specs/show_spec.js | 102 -- webpack/common.config.js | 152 +++ webpack/dev.config.js | 29 + webpack/prod.config.js | 37 + 113 files changed, 2127 insertions(+), 4947 deletions(-) create mode 100644 .babelrc create mode 100644 .bootstraprc create mode 100644 app/l8pr/static/actions/auth.js create mode 100644 app/l8pr/static/actions/browser.js create mode 100644 app/l8pr/static/actions/data.js create mode 100644 app/l8pr/static/actions/player.js delete mode 100644 app/l8pr/static/api.service.js create mode 100644 app/l8pr/static/app.js create mode 100644 app/l8pr/static/components/Loop/index.js create mode 100644 app/l8pr/static/components/Loop/style.scss create mode 100644 app/l8pr/static/components/LoopItem/index.js create mode 100644 app/l8pr/static/components/LoopItem/style.scss create mode 100644 app/l8pr/static/components/NavPlayer/index.js create mode 100644 app/l8pr/static/components/NavPlayer/style.scss create mode 100644 app/l8pr/static/components/Screen/index.js create mode 100644 app/l8pr/static/components/Screen/style.scss create mode 100644 app/l8pr/static/components/index.js create mode 100644 app/l8pr/static/constants/index.js create mode 100644 app/l8pr/static/containers/Controller/index.js create mode 100644 app/l8pr/static/containers/Home/index.js create mode 100644 app/l8pr/static/containers/Home/style.scss create mode 100644 app/l8pr/static/containers/Login/index.js create mode 100644 app/l8pr/static/containers/NotFound/index.js create mode 100644 app/l8pr/static/containers/Protected/index.js create mode 100644 app/l8pr/static/containers/Root/DevTools.js create mode 100644 app/l8pr/static/containers/Root/Root.dev.js create mode 100644 app/l8pr/static/containers/Root/Root.js create mode 100644 app/l8pr/static/containers/Root/Root.prod.js create mode 100644 app/l8pr/static/containers/Strip/index.js create mode 100644 app/l8pr/static/containers/Strip/style.scss create mode 100644 app/l8pr/static/containers/StripHeader/index.js create mode 100644 app/l8pr/static/containers/StripHeader/style.scss create mode 100644 app/l8pr/static/containers/index.js create mode 100644 app/l8pr/static/index.html create mode 100644 app/l8pr/static/index.js delete mode 100644 app/l8pr/static/login/module.js delete mode 100644 app/l8pr/static/login/template.html delete mode 100644 app/l8pr/static/main.html delete mode 100644 app/l8pr/static/main.js delete mode 100644 app/l8pr/static/main.less delete mode 100644 app/l8pr/static/player/giphy.js delete mode 100644 app/l8pr/static/player/player.ctrl.js delete mode 100644 app/l8pr/static/player/player.service.js delete mode 100644 app/l8pr/static/player/soundcloud/module.js delete mode 100644 app/l8pr/static/player/vimeo/module.js delete mode 100644 app/l8pr/static/player/webtorrent/module.js delete mode 100644 app/l8pr/static/player/webtorrent/style.less delete mode 100644 app/l8pr/static/player/youtube/module.js create mode 100644 app/l8pr/static/reducers/auth.js create mode 100644 app/l8pr/static/reducers/browser.js create mode 100644 app/l8pr/static/reducers/data.js create mode 100644 app/l8pr/static/reducers/index.js create mode 100644 app/l8pr/static/reducers/player.js create mode 100644 app/l8pr/static/routes.js create mode 100644 app/l8pr/static/selectors.js create mode 100644 app/l8pr/static/store/configureStore.dev.js create mode 100644 app/l8pr/static/store/configureStore.js create mode 100644 app/l8pr/static/store/configureStore.prod.js delete mode 100644 app/l8pr/static/strip/add-to-shows/module.js delete mode 100644 app/l8pr/static/strip/add-to-shows/template.html delete mode 100644 app/l8pr/static/strip/feed/module.js delete mode 100644 app/l8pr/static/strip/feed/style.less delete mode 100644 app/l8pr/static/strip/feed/template.html delete mode 100644 app/l8pr/static/strip/header/module.js delete mode 100644 app/l8pr/static/strip/header/style.less delete mode 100644 app/l8pr/static/strip/header/template.html delete mode 100644 app/l8pr/static/strip/help/module.js delete mode 100644 app/l8pr/static/strip/help/style.less delete mode 100644 app/l8pr/static/strip/help/template.html delete mode 100644 app/l8pr/static/strip/loop/module.js delete mode 100644 app/l8pr/static/strip/loop/style.less delete mode 100644 app/l8pr/static/strip/loop/template.html delete mode 100644 app/l8pr/static/strip/module.js delete mode 100644 app/l8pr/static/strip/reset-password/module.js delete mode 100644 app/l8pr/static/strip/reset-password/template.html delete mode 100644 app/l8pr/static/strip/search-results/module.js delete mode 100644 app/l8pr/static/strip/search-results/style.less delete mode 100644 app/l8pr/static/strip/search-results/template.html delete mode 100644 app/l8pr/static/strip/show-config/module.js delete mode 100644 app/l8pr/static/strip/show-config/template.html delete mode 100644 app/l8pr/static/strip/show/module.js delete mode 100644 app/l8pr/static/strip/show/style.less delete mode 100644 app/l8pr/static/strip/show/template.html delete mode 100644 app/l8pr/static/strip/style.less create mode 100644 app/l8pr/static/styles/config/_colors.scss create mode 100644 app/l8pr/static/styles/config/_fonts.scss create mode 100644 app/l8pr/static/styles/config/_reset.scss create mode 100644 app/l8pr/static/styles/config/_typography.scss create mode 100644 app/l8pr/static/styles/config/_variables.scss create mode 100644 app/l8pr/static/styles/font-awesome.config.js create mode 100644 app/l8pr/static/styles/font-awesome.config.less create mode 100644 app/l8pr/static/styles/font-awesome.config.prod.js create mode 100644 app/l8pr/static/styles/main.scss create mode 100644 app/l8pr/static/styles/theme/_base.scss create mode 100644 app/l8pr/static/styles/theme/_footer.scss create mode 100644 app/l8pr/static/styles/theme/_login.scss create mode 100644 app/l8pr/static/styles/theme/_navbar.scss create mode 100644 app/l8pr/static/styles/utils/_margins.scss create mode 100644 app/l8pr/static/utils/config.js create mode 100644 app/l8pr/static/utils/index.js create mode 100644 app/l8pr/static/utils/requireAuthentication.js delete mode 100644 app/l8pr/static/variables.less delete mode 100644 app/l8pr/templates/index.html delete mode 100644 protractor.js delete mode 100644 specs/show_spec.js create mode 100644 webpack/common.config.js create mode 100644 webpack/dev.config.js create mode 100644 webpack/prod.config.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..2f5420e --- /dev/null +++ b/.babelrc @@ -0,0 +1,16 @@ +{ + // need loose mode for IE9 https://phabricator.babeljs.io/T3041 + "presets": [ + [ + "es2015", + { + "loose": true + } + ], + "stage-1", + "react" + ], + "plugins": [ + "transform-decorators-legacy" + ] +} \ No newline at end of file diff --git a/.bootstraprc b/.bootstraprc new file mode 100644 index 0000000..1854a93 --- /dev/null +++ b/.bootstraprc @@ -0,0 +1,117 @@ +--- +# Output debugging info +# loglevel: debug + +# Major version of Bootstrap: 3 or 4 +bootstrapVersion: 3 + +# If Bootstrap version 3 is used - turn on/off custom icon font path +useCustomIconFontPath: false + +# Webpack loaders, order matters +styleLoaders: + - style-loader + - css-loader + - sass-loader + +# Extract styles to stand-alone css file +# Different settings for different environments can be used, +# It depends on value of NODE_ENV environment variable +# This param can also be set in webpack config: +# entry: 'bootstrap-loader/extractStyles' +# extractStyles: false +extractStyles: true +# env: +# development: +# extractStyles: false +# production: +# extractStyles: true + + +# Customize Bootstrap variables that get imported before the original Bootstrap variables. +# Thus, derived Bootstrap variables can depend on values from here. +# See the Bootstrap _variables.scss file for examples of derived Bootstrap variables. +# +# preBootstrapCustomizations: ./path/to/bootstrap/pre-customizations.scss + + +# This gets loaded after bootstrap/variables is loaded +# Thus, you may customize Bootstrap variables +# based on the values established in the Bootstrap _variables.scss file +# +# bootstrapCustomizations: ./path/to/bootstrap/customizations.scss + + +# Import your custom styles here +# Usually this endpoint-file contains list of @imports of your application styles +# +# appStyles: ./path/to/your/app/styles/endpoint.scss + + +### Bootstrap styles +styles: + + # Mixins + mixins: true + + # Reset and dependencies + normalize: true + print: true + glyphicons: true + + # Core CSS + scaffolding: true + type: true + code: true + grid: true + tables: true + forms: true + buttons: true + + # Components + component-animations: true + dropdowns: true + button-groups: true + input-groups: true + navs: true + navbar: true + breadcrumbs: true + pagination: true + pager: true + labels: true + badges: true + jumbotron: true + thumbnails: true + alerts: true + progress-bars: true + media: true + list-group: true + panels: true + wells: true + responsive-embed: true + close: true + + # Components w/ JavaScript + modals: true + tooltip: true + popovers: true + carousel: true + + # Utility classes + utilities: true + responsive-utilities: true + +### Bootstrap scripts +scripts: + transition: true + alert: true + button: true + carousel: true + collapse: true + dropdown: true + modal: true + tooltip: true + popover: true + scrollspy: true + tab: true + affix: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 06b6ec0..a61e4da 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ app/l8pr/static/CACHE app/l8pr/static/bower_components staticfiles/ uploaded/ +static_dist/ diff --git a/app/l8pr/static/actions/auth.js b/app/l8pr/static/actions/auth.js new file mode 100644 index 0000000..de2141f --- /dev/null +++ b/app/l8pr/static/actions/auth.js @@ -0,0 +1,92 @@ +import fetch from 'isomorphic-fetch'; +import { push } from 'react-router-redux'; +import { SERVER_URL } from '../utils/config'; +import { checkHttpStatus, parseJSON } from '../utils'; +import { + AUTH_LOGIN_USER_REQUEST, + AUTH_LOGIN_USER_FAILURE, + AUTH_LOGIN_USER_SUCCESS, + AUTH_LOGOUT_USER +} from '../constants'; + +export function authLoginUserSuccess(token, user) { + sessionStorage.setItem('token', token); + sessionStorage.setItem('user', JSON.stringify(user)); + return { + type: AUTH_LOGIN_USER_SUCCESS, + payload: { + token, + user + } + }; +} + +export function authLoginUserFailure(error, message) { + sessionStorage.removeItem('token'); + return { + type: AUTH_LOGIN_USER_FAILURE, + payload: { + status: error, + statusText: message + } + }; +} + +export function authLoginUserRequest() { + return { + type: AUTH_LOGIN_USER_REQUEST + } +} + +export function authLogout() { + sessionStorage.removeItem('token'); + sessionStorage.removeItem('user'); + return { + type: AUTH_LOGOUT_USER + }; +} + +export function authLogoutAndRedirect() { + return (dispatch, state) => { + dispatch(authLogout()); + dispatch(push('/login')); + return Promise.resolve(); // TODO: we need a promise here because of the tests, find a better way + }; +} + +export function authLoginUser(email, password, redirect = '/') { + return (dispatch) => { + dispatch(authLoginUserRequest()); + const auth = btoa(`${email}:${password}`); + return fetch(`${SERVER_URL}/api/v1/accounts/login/`, { + method: 'post', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `Basic ${auth}` + } + }) + .then(checkHttpStatus) + .then(parseJSON) + .then((response) => { + dispatch(authLoginUserSuccess(response.token, response.user)); + dispatch(push(redirect)); + }) + .catch((error) => { + if (error && typeof error.response !== 'undefined' && error.response.status === 401) { + // Invalid authentication credentials + return error.response.json().then((data) => { + dispatch(authLoginUserFailure(401, data.non_field_errors[0])); + }); + } else if (error && typeof error.response !== 'undefined' && error.response.status >= 500) { + // Server side error + dispatch(authLoginUserFailure(500, 'A server error occurred while sending your data!')); + } else { + // Most likely connection issues + dispatch(authLoginUserFailure('Connection Error', 'An error occurred while sending your data!')); + } + + return Promise.resolve(); // TODO: we need a promise here because of the tests, find a better way + }); + }; +} diff --git a/app/l8pr/static/actions/browser.js b/app/l8pr/static/actions/browser.js new file mode 100644 index 0000000..de37052 --- /dev/null +++ b/app/l8pr/static/actions/browser.js @@ -0,0 +1,54 @@ +import fetch from 'isomorphic-fetch'; +import { push } from 'react-router-redux'; + +import { SERVER_URL } from '../utils/config'; +import { checkHttpStatus, parseJSON } from '../utils'; +import * as c from '../constants'; +import { authLoginUserFailure } from './auth'; + +export function openStrip(){ + return { + type: c.SET_STRIP_STATE, + payload: true, + } +} +export function closeStrip(){ + return { + type: c.SET_STRIP_STATE, + payload: false, + } +} +export function toggleStrip() { + return (dispatch, getState) => { + if (getState().browser.stripOpened) { + dispatch(closeStrip()) + } else { + dispatch(openStrip()) + } + } +} +function receiveLoop(loop) { + return { + type: c.RECEIVE_LOOP, + payload: loop, + } +} + +export function fetchLoop(userId=2) { + return (dispatch) => ( + fetch(`${SERVER_URL}/api/loops/${userId}/`, { + // credentials: 'include', + headers: { + Accept: 'application/json', + // Authorization: `Token ${token}` + } + }) + .then(checkHttpStatus) + .then(parseJSON) + .then((response) => { + const loop = response.shows_list + dispatch(receiveLoop(loop)) + return loop + }) + ) +} diff --git a/app/l8pr/static/actions/data.js b/app/l8pr/static/actions/data.js new file mode 100644 index 0000000..22ab2b3 --- /dev/null +++ b/app/l8pr/static/actions/data.js @@ -0,0 +1,58 @@ +import fetch from 'isomorphic-fetch'; +import { push } from 'react-router-redux'; + +import { SERVER_URL } from '../utils/config'; +import { checkHttpStatus, parseJSON } from '../utils'; +import { DATA_FETCH_PROTECTED_DATA_REQUEST, DATA_RECEIVE_PROTECTED_DATA, RECEIVE_LOOP } from '../constants'; +import { authLoginUserFailure } from './auth'; + +export function dataReceiveProtectedData(data) { + return { + type: DATA_RECEIVE_PROTECTED_DATA, + payload: { + data + } + }; +} + +export function dataFetchProtectedDataRequest() { + return { + type: DATA_FETCH_PROTECTED_DATA_REQUEST + }; +} + +export function dataFetchProtectedData(token) { + return (dispatch, state) => { + dispatch(dataFetchProtectedDataRequest()); + return fetch(`${SERVER_URL}/api/v1/getdata/`, { + credentials: 'include', + headers: { + Accept: 'application/json', + Authorization: `Token ${token}` + } + }) + .then(checkHttpStatus) + .then(parseJSON) + .then((response) => { + dispatch(dataReceiveProtectedData(response.data)); + }) + .catch((error) => { + if (error && typeof error.response !== 'undefined' && error.response.status === 401) { + // Invalid authentication credentials + return error.response.json().then((data) => { + dispatch(authLoginUserFailure(401, data.non_field_errors[0])); + dispatch(push('/login')); + }); + } else if (error && typeof error.response !== 'undefined' && error.response.status >= 500) { + // Server side error + dispatch(authLoginUserFailure(500, 'A server error occurred while sending your data!')); + } else { + // Most likely connection issues + dispatch(authLoginUserFailure('Connection Error', 'An error occurred while sending your data!')); + } + + dispatch(push('/login')); + return Promise.resolve(); // TODO: we need a promise here because of the tests, find a better way + }); + }; +} diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js new file mode 100644 index 0000000..a4aa0c0 --- /dev/null +++ b/app/l8pr/static/actions/player.js @@ -0,0 +1,51 @@ +import fetch from 'isomorphic-fetch'; +import { SERVER_URL } from '../utils/config'; +import { checkHttpStatus, parseJSON } from '../utils'; +import * as c from '../constants'; +import * as selectors from '../selectors' +import { get } from 'lodash' +import { push } from 'react-router-redux' + +export function setPlaylist(playlist) { + return { + type: c.SET_PLAYLIST, + payload: playlist, + } +} + +export function play(showAndItem) { + return (dispatch, getState) => { + dispatch({ + type: c.PLAY, + payload: showAndItem, + }) + const show = selectors.currentShow(getState()) + const item = selectors.currentTrack(getState()) + dispatch(push(`/show/${show}/item/${item}`)) + } +} + +export function next() { + return (dispatch, getState) => { + let nextItem = undefined + let nextShow = undefined + const currentShow = selectors.getCurrentShow(getState()) + const itemPositionInShow = selectors.getCurrentTrackPositionInShow(getState()) + if (itemPositionInShow < currentShow.items.length - 1) { + nextItem = currentShow.items[itemPositionInShow + 1] + } else { + const showPosition = selectors.getCurrentShowPositionInShow(getState()) + const playlist = selectors.playlist(getState()) + nextShow = playlist[(showPosition + 1) % playlist.length] + nextItem = nextShow.items[0] + } + return dispatch(play({ + item: get(nextItem, 'id'), + show: get(nextShow, 'id'), + })) + } +} + +export function previous() { + return { type: c.PREVIOUS } +} diff --git a/app/l8pr/static/api.service.js b/app/l8pr/static/api.service.js deleted file mode 100644 index bc9262a..0000000 --- a/app/l8pr/static/api.service.js +++ /dev/null @@ -1,139 +0,0 @@ -(function() { - 'use strict'; - - Api.$inject = ['Restangular']; - function Api(Restangular) { - var self = { - Accounts: Restangular.service('users'), - Auth: Restangular.service('auth'), - Register: Restangular.service('register'), - Items: Restangular.service('items'), - Search: Restangular.service('search'), - SearchYoutube: Restangular.service('youtube'), - GetItemMetadata: Restangular.service('metadata'), - Shows: (function() { - Restangular.extendModel('shows', function(model) { - model.duration = function() { - if (angular.isDefined(model.items)) { - return model.items.reduce(function(a, b) {return a + b.duration;}, 0); - } - }; - model.listTypes = function() { - var types = model.items.map(function(link) { - return link.provider_name; - }); - if (_.contains(types, 'SoundCloud') && model.settings.giphy) { - types.push('Giphy'); - } - return _.unique(types); - }; - return model; - }); - return Restangular.service('shows'); - })(), - LatestItems: function() { - var items = []; - return self.Shows.getList({ordering: '-updated', limit: 10}).then(function(shows) { - shows.forEach(function(show) { - show.items.slice(0, 5).forEach(function(item) { - item.show = angular.copy(show); - items.push(item); - }); - }); - return items; - }); - }, - Loops: (function() { - Restangular.extendModel('loops', function(model) { - if (angular.isDefined(model.shows_list)) { - model.shows_list = model.shows_list.map(function(show) { - return Restangular.restangularizeElement(null, show, 'shows'); - }); - } - model.duration = function() { - if (angular.isDefined(model.shows_list)) { - return model.shows_list.reduce(function(a, b) {return a + b.duration();}, 0); - } - }; - return model; - }); - return Restangular.service('loops'); - })(), - FindOrCreateItem: (function() { - return function(item) { - if (item.id) { - return item; - } - return self.Items.getList({url: item.url}).then(function(items) { - if (items.length === 0) { - return self.Items.post({url: item.url}).then(function(item) { - return item; - }); - } - return items[0]; - }); - }; - })(), - FindOrCreateInbox: (function() { - return function(params) { - return self.Shows.getList({show_type: 'inbox', user: params.user}).then(function(shows) { - var show; - if (shows.length === 0) { - show = angular.extend({}, { - show_type: 'inbox', - title: 'my inbox', - items: [] - }, show); - return self.Shows.post(show).then(function(show) { - return show; - }); - } - if (shows.length > 1) { - throw {message: 'We found more than 1 inbox', params: params}; - } - return shows[0]; - }); - }; - })() - }; - return self; - } - - angular.module('loopr.api', ['restangular', 'LocalStorageModule']) - .config(['localStorageServiceProvider', function(localStorageServiceProvider) { - localStorageServiceProvider - .setPrefix('loopr') - .setStorageType('localStorage') - .setNotify(true, true); - }]) - .factory('Api', Api) - .config(['RestangularProvider', function(RestangularProvider) { - RestangularProvider.setBaseUrl('/api'); - RestangularProvider.setRequestSuffix('/'); - RestangularProvider.addResponseInterceptor(function(data, operation, what, url, response, deferred) { - var extractedData = data; - // .. to look for getList operations - if (operation === 'getList' && !Array.isArray(extractedData) && extractedData.results) { - extractedData = extractedData.results; - } - return extractedData; - }); - // X-CSRFToken - function getCookie(name) { - var cookieValue = null; - if (document.cookie && document.cookie !== '') { - var cookies = document.cookie.split(';'); - for (var i = 0; i < cookies.length; i++) { - var cookie = $.trim(cookies[i]); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) === (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; - } - RestangularProvider.setDefaultHeaders({'X-CSRFToken': getCookie('csrftoken')}); - }]); -})(); diff --git a/app/l8pr/static/app.js b/app/l8pr/static/app.js new file mode 100644 index 0000000..718af86 --- /dev/null +++ b/app/l8pr/static/app.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { Link } from 'react-router'; +import { connect } from 'react-redux'; +import { push } from 'react-router-redux'; +import classNames from 'classnames'; + +import { authLogoutAndRedirect } from './actions/auth'; +import './styles/main.scss'; + +class App extends React.Component { + + static propTypes = { + isAuthenticated: React.PropTypes.bool.isRequired, + children: React.PropTypes.shape().isRequired, + dispatch: React.PropTypes.func.isRequired, + pathName: React.PropTypes.string.isRequired + }; + + logout = () => { + this.props.dispatch(authLogoutAndRedirect()); + }; + + goToIndex = () => { + this.props.dispatch(push('/')); + }; + + goToProtected = () => { + this.props.dispatch(push('/protected')); + }; + + render() { + const homeClass = classNames({ + active: this.props.pathName === '/' + }); + const protectedClass = classNames({ + active: this.props.pathName === '/protected' + }); + const loginClass = classNames({ + active: this.props.pathName === '/login' + }); + + return ( +
+
+ {this.props.children} +
+
+ ); + } +} + +const mapStateToProps = (state, ownProps) => { + return { + isAuthenticated: state.auth.isAuthenticated, + pathName: ownProps.location.pathname + }; +}; + +export default connect(mapStateToProps)(App); +export { App as AppNotConnected }; diff --git a/app/l8pr/static/components/Loop/index.js b/app/l8pr/static/components/Loop/index.js new file mode 100644 index 0000000..797fc56 --- /dev/null +++ b/app/l8pr/static/components/Loop/index.js @@ -0,0 +1,43 @@ +import React from 'react' +import { LoopItem } from '../index' +import './style.scss' + +export default class Loop extends React.Component { + constructor(props) { + super() + this.state = { + offset: 0, + perPage: 3, + } + } + previous() { + this.setState({ offset: this.state.offset - (this.state.perPage - 1) }) + } + next() { + this.setState({ offset: this.state.offset + (this.state.perPage - 1) }) + } + render() { + const items = this.props.items.slice(this.state.offset, this.state.offset + this.state.perPage) + return ( +
+
+ navigate_before +
+
    + {items.map(i => ( +
  • + +
  • + ))} +
+
+ navigate_next +
+
+ ) + } +} + +Loop.propTypes = { + items: React.PropTypes.array.isRequired +} diff --git a/app/l8pr/static/components/Loop/style.scss b/app/l8pr/static/components/Loop/style.scss new file mode 100644 index 0000000..7d26a88 --- /dev/null +++ b/app/l8pr/static/components/Loop/style.scss @@ -0,0 +1,21 @@ +.Loop { + position: relative; + white-space: nowrap; + height: 200px; + li { + display: inline-block; + height: 200px; + overflow: hidden; + } + .Loop__nav { + position: absolute; + top: 50; + bottom: 0; + } + .Loop__nav--prev { + left: 0; + } + .Loop__nav--next { + right: 0; + } +} diff --git a/app/l8pr/static/components/LoopItem/index.js b/app/l8pr/static/components/LoopItem/index.js new file mode 100644 index 0000000..4f9c483 --- /dev/null +++ b/app/l8pr/static/components/LoopItem/index.js @@ -0,0 +1,20 @@ +import React from 'react' +import './style.scss' + +export default function LoopItem({ item }) { + return ( +
+
+

{item.title}

+ {item.description} +
+
+ +
+
+ ) +} + +LoopItem.propTypes = { + item: React.PropTypes.object.isRequired +} diff --git a/app/l8pr/static/components/LoopItem/style.scss b/app/l8pr/static/components/LoopItem/style.scss new file mode 100644 index 0000000..3a417c5 --- /dev/null +++ b/app/l8pr/static/components/LoopItem/style.scss @@ -0,0 +1,2 @@ +.LoopItem { +} diff --git a/app/l8pr/static/components/NavPlayer/index.js b/app/l8pr/static/components/NavPlayer/index.js new file mode 100644 index 0000000..c4a51f4 --- /dev/null +++ b/app/l8pr/static/components/NavPlayer/index.js @@ -0,0 +1,36 @@ +import React from 'react' +import './style.scss' + +export default function NavPlayer({ + onPreviousShow, + onPreviousItem, + onPlay, + onNextItem, + onNextShow, + onMute, +}) { + return ( +
+
+ + first_page + + + chevron_left + + + play_arrow + + + chevron_right + + + last_page + + + volume_up + +
+
+ ) +} diff --git a/app/l8pr/static/components/NavPlayer/style.scss b/app/l8pr/static/components/NavPlayer/style.scss new file mode 100644 index 0000000..5553764 --- /dev/null +++ b/app/l8pr/static/components/NavPlayer/style.scss @@ -0,0 +1,3 @@ +.NavPlayer { + background-color: rgba(255, 255, 255, 0.85); +} diff --git a/app/l8pr/static/components/Screen/index.js b/app/l8pr/static/components/Screen/index.js new file mode 100644 index 0000000..ca689a5 --- /dev/null +++ b/app/l8pr/static/components/Screen/index.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactPlayer from 'react-player' +import './style.scss'; + +export default class Screen extends React.Component { + + render() { + return ( + + ) + } +} diff --git a/app/l8pr/static/components/Screen/style.scss b/app/l8pr/static/components/Screen/style.scss new file mode 100644 index 0000000..f06958b --- /dev/null +++ b/app/l8pr/static/components/Screen/style.scss @@ -0,0 +1,7 @@ +.Screen { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; +} diff --git a/app/l8pr/static/components/index.js b/app/l8pr/static/components/index.js new file mode 100644 index 0000000..dd2f51b --- /dev/null +++ b/app/l8pr/static/components/index.js @@ -0,0 +1,3 @@ +export Screen from './Screen' +export Loop from './Loop' +export LoopItem from './LoopItem' diff --git a/app/l8pr/static/constants/index.js b/app/l8pr/static/constants/index.js new file mode 100644 index 0000000..ddf63d5 --- /dev/null +++ b/app/l8pr/static/constants/index.js @@ -0,0 +1,21 @@ +export const AUTH_LOGIN_USER_REQUEST = 'AUTH_LOGIN_USER_REQUEST'; +export const AUTH_LOGIN_USER_FAILURE = 'AUTH_LOGIN_USER_FAILURE'; +export const AUTH_LOGIN_USER_SUCCESS = 'AUTH_LOGIN_USER_SUCCESS'; +export const AUTH_LOGOUT_USER = 'AUTH_LOGOUT_USER'; + +export const DATA_FETCH_PROTECTED_DATA_REQUEST = 'DATA_FETCH_PROTECTED_DATA_REQUEST'; +export const DATA_RECEIVE_PROTECTED_DATA = 'DATA_RECEIVE_PROTECTED_DATA'; + +export const SET_LOOP = 'SET_LOOP'; +export const SET_SHOWS = 'SET_SHOWS'; +export const PLAY_ITEM = 'PLAY_ITEM'; +export const PAUSE = 'PAUSE'; +export const PLAY = 'PLAY'; +export const MUTE = 'MUTE'; +export const UNMUTE = 'UNMUTE'; +export const INSERT_SHOW = 'INSERT_SHOW'; + +export const RECEIVE_LOOP = 'RECEIVE_LOOP' + +export const SET_PLAYLIST = 'SET_PLAYLIST' +export const SET_STRIP_STATE = 'SET_STRIP_STATE' diff --git a/app/l8pr/static/containers/Controller/index.js b/app/l8pr/static/containers/Controller/index.js new file mode 100644 index 0000000..3bf382b --- /dev/null +++ b/app/l8pr/static/containers/Controller/index.js @@ -0,0 +1,14 @@ +import React from 'react' +import { connect } from 'react-redux' +import NavPlayer from '../../components/NavPlayer' +import { next } from '../../actions/player' + +function ControllerComponent(props) { + return ( + + ) +} +const mapDispatchToProps = (dispatch) => ({ + onNextItem: () => (dispatch(next())), +}) +export default connect(null, mapDispatchToProps)(ControllerComponent) diff --git a/app/l8pr/static/containers/Home/index.js b/app/l8pr/static/containers/Home/index.js new file mode 100644 index 0000000..7c49cce --- /dev/null +++ b/app/l8pr/static/containers/Home/index.js @@ -0,0 +1,53 @@ +import React from 'react' +import { Link } from 'react-router' +import { connect } from 'react-redux' +import { Screen } from '../../components' +import { Strip } from '../index' +import { fetchLoop } from '../../actions/browser' +import { setPlaylist, play, next } from '../../actions/player' +import * as selectors from '../../selectors' +import './style.scss' + +class HomeView extends React.Component { + + componentWillMount() { + this.props.fetchLoop(this.props.location) + .then((loop) => this.props.setPlaylist(loop)) + .then(() => this.props.play(this.props.location)) + } + + static propTypes = { + media: React.PropTypes.object + } + + render() { + return ( +
+ {this.props.media && + + } + +
+ ) + } +} + +const mapStateToProps = (state) => { + return { + media: selectors.getCurrentTrack(state), + location: selectors.getLocation(state), + } +} + +const mapDispatchToProps = (dispatch) => ({ + nextItem: () => (dispatch(next())), + fetchLoop: () => (dispatch(fetchLoop())), + setPlaylist: (loop) => (dispatch(setPlaylist(loop))), + play: (args) => (dispatch(play(args))), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(HomeView) +export { HomeView as HomeViewNotConnected } diff --git a/app/l8pr/static/containers/Home/style.scss b/app/l8pr/static/containers/Home/style.scss new file mode 100644 index 0000000..7192bba --- /dev/null +++ b/app/l8pr/static/containers/Home/style.scss @@ -0,0 +1,8 @@ +.Home { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + font-family: 'Montserrat', 'Trebuchet Ms', sans-serif, "Helvetica Neue", Helvetica, Arial, sans-serif; +} diff --git a/app/l8pr/static/containers/Login/index.js b/app/l8pr/static/containers/Login/index.js new file mode 100644 index 0000000..30429b9 --- /dev/null +++ b/app/l8pr/static/containers/Login/index.js @@ -0,0 +1,134 @@ +import React from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import classNames from 'classnames'; +import { push } from 'react-router-redux'; +import t from 'tcomb-form'; + +import * as actionCreators from '../../actions/auth'; + +const Form = t.form.Form; + +const Login = t.struct({ + email: t.String, + password: t.String +}); + +const LoginFormOptions = { + auto: 'placeholders', + help: Hint: a@a.com / qw, + fields: { + password: { + type: 'password' + } + } +}; + +class LoginView extends React.Component { + + static propTypes = { + dispatch: React.PropTypes.func.isRequired, + isAuthenticated: React.PropTypes.bool.isRequired, + isAuthenticating: React.PropTypes.bool.isRequired, + statusText: React.PropTypes.string, + actions: React.PropTypes.shape({ + authLoginUser: React.PropTypes.func.isRequired + }).isRequired, + location: React.PropTypes.shape({ + query: React.PropTypes.object.isRequired + }) + }; + + constructor(props) { + super(props); + + const redirectRoute = this.props.location ? this.props.location.query.next || '/' : '/'; + this.state = { + formValues: { + email: '', + password: '' + }, + redirectTo: redirectRoute + }; + } + + componentWillMount() { + if (this.props.isAuthenticated) { + this.props.dispatch(push('/')); + } + } + + onFormChange = (value) => { + this.setState({ formValues: value }); + }; + + login = (e) => { + e.preventDefault(); + const value = this.loginForm.getValue(); + if (value) { + this.props.actions.authLoginUser(value.email, value.password, this.state.redirectTo); + } + }; + + render() { + let statusText = null; + if (this.props.statusText) { + const statusTextClassNames = classNames({ + 'alert': true, + 'alert-danger': this.props.statusText.indexOf('Authentication Error') === 0, + 'alert-success': this.props.statusText.indexOf('Authentication Error') !== 0 + }); + + statusText = ( +
+
+
+ {this.props.statusText} +
+
+
+ ); + } + + return ( +
+

Login

+
+ {statusText} +
+ { this.loginForm = ref; }} + type={Login} + options={LoginFormOptions} + value={this.state.formValues} + onChange={this.onFormChange} + /> + +
+
+
+ ); + } +} + +const mapStateToProps = (state) => { + return { + isAuthenticated: state.auth.isAuthenticated, + isAuthenticating: state.auth.isAuthenticating, + statusText: state.auth.statusText + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + dispatch, + actions: bindActionCreators(actionCreators, dispatch) + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(LoginView); +export { LoginView as LoginViewNotConnected }; diff --git a/app/l8pr/static/containers/NotFound/index.js b/app/l8pr/static/containers/NotFound/index.js new file mode 100644 index 0000000..f5ad26a --- /dev/null +++ b/app/l8pr/static/containers/NotFound/index.js @@ -0,0 +1,13 @@ +import React from 'react'; + +export default class NotFoundView extends React.Component { + + render() { + return ( +
+

NOT FOUND

+
+ ); + } + +} diff --git a/app/l8pr/static/containers/Protected/index.js b/app/l8pr/static/containers/Protected/index.js new file mode 100644 index 0000000..bbea1f2 --- /dev/null +++ b/app/l8pr/static/containers/Protected/index.js @@ -0,0 +1,83 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import * as actionCreators from '../../actions/data'; + +class ProtectedView extends React.Component { + + static propTypes = { + isFetching: React.PropTypes.bool.isRequired, + data: React.PropTypes.string, + token: React.PropTypes.string.isRequired, + actions: React.PropTypes.shape({ + dataFetchProtectedData: React.PropTypes.func.isRequired + }).isRequired + }; + + // Note: have to use componentWillMount, if I add this in constructor will get error: + // Warning: setState(...): Cannot update during an existing state transition (such as within `render`). + // Render methods should be a pure function of props and state. + componentWillMount() { + const token = this.props.token; + this.props.actions.dataFetchProtectedData(token); + } + + render() { + return ( +
+
+

Protected

+ {this.props.isFetching === true ? +

Loading data...

+ : +
+

Data received from the server:

+
+
+ {this.props.data} +
+
+
+
How does this work?
+

+ On the componentWillMount method of the +  ProtectedView component, the action +  dataFetchProtectedData is called. This action will first + dispatch a DATA_FETCH_PROTECTED_DATA_REQUEST action to the Redux + store. When an action is dispatched to the store, an appropriate reducer for + that specific action will change the state of the store. After that it will then + make an asynchronous request to the server using + the isomorphic-fetch library. On its + response, it will dispatch the DATA_RECEIVE_PROTECTED_DATA action + to the Redux store. In case of wrong credentials in the request, the  + AUTH_LOGIN_USER_FAILURE action will be dispatched. +

+

+ Because the ProtectedView is connected to the Redux store, when the + value of a property connected to the view is changed, the view is re-rendered + with the new data. +

+
+
+ } +
+
+ ); + } +} + +const mapStateToProps = (state) => { + return { + data: state.data.data, + isFetching: state.data.isFetching + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + actions: bindActionCreators(actionCreators, dispatch) + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ProtectedView); +export { ProtectedView as ProtectedViewNotConnected }; diff --git a/app/l8pr/static/containers/Root/DevTools.js b/app/l8pr/static/containers/Root/DevTools.js new file mode 100644 index 0000000..72248eb --- /dev/null +++ b/app/l8pr/static/containers/Root/DevTools.js @@ -0,0 +1,23 @@ +/* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ + +import React from 'react'; +import { createDevTools } from 'redux-devtools'; + +// Monitors are separate packages, and you can make a custom one +import LogMonitor from 'redux-devtools-log-monitor'; +import DockMonitor from 'redux-devtools-dock-monitor'; + +// createDevTools takes a monitor and produces a DevTools component +const DevTools = createDevTools( + // Monitors are individually adjustable with props. + // Consult their repositories to learn about those props. + // Here, we put LogMonitor inside a DockMonitor. + + + +); + +export default DevTools; diff --git a/app/l8pr/static/containers/Root/Root.dev.js b/app/l8pr/static/containers/Root/Root.dev.js new file mode 100644 index 0000000..eca0fe4 --- /dev/null +++ b/app/l8pr/static/containers/Root/Root.dev.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router'; + +import routes from '../../routes'; +import DevTools from './DevTools'; + +export default class Root extends React.Component { + + static propTypes = { + store: React.PropTypes.shape().isRequired, + history: React.PropTypes.shape().isRequired + }; + + render() { + return ( +
+ +
+ + {routes} + + +
+
+
+ ); + } +} diff --git a/app/l8pr/static/containers/Root/Root.js b/app/l8pr/static/containers/Root/Root.js new file mode 100644 index 0000000..b321b61 --- /dev/null +++ b/app/l8pr/static/containers/Root/Root.js @@ -0,0 +1,5 @@ +if (process.env.NODE_ENV === 'production') { + module.exports = require('./Root.prod'); // eslint-disable-line global-require +} else { + module.exports = require('./Root.dev'); // eslint-disable-line global-require +} diff --git a/app/l8pr/static/containers/Root/Root.prod.js b/app/l8pr/static/containers/Root/Root.prod.js new file mode 100644 index 0000000..23b875a --- /dev/null +++ b/app/l8pr/static/containers/Root/Root.prod.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router'; + +import routes from '../../routes'; + +export default class Root extends React.Component { + + static propTypes = { + store: React.PropTypes.shape().isRequired, + history: React.PropTypes.shape().isRequired + }; + + render() { + return ( +
+ +
+ + {routes} + +
+
+
+ ); + } +} diff --git a/app/l8pr/static/containers/Strip/index.js b/app/l8pr/static/containers/Strip/index.js new file mode 100644 index 0000000..1d7e198 --- /dev/null +++ b/app/l8pr/static/containers/Strip/index.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Loop } from '../../components' +import { StripHeader, Controller} from '../index' +import './style.scss' + +class Strip extends React.Component { + static propTypes = { + }; + + render() { + const { stripOpened, loopItems } = this.props + return ( +
+ + { stripOpened && + + } + +
+ ) + } +} + +const mapStateToProps = (state) => ({ + stripOpened: state.browser.stripOpened, + loopItems: state.browser.loop +}) +export default connect(mapStateToProps)(Strip) diff --git a/app/l8pr/static/containers/Strip/style.scss b/app/l8pr/static/containers/Strip/style.scss new file mode 100644 index 0000000..aaa5794 --- /dev/null +++ b/app/l8pr/static/containers/Strip/style.scss @@ -0,0 +1,6 @@ +.Strip { + position: absolute; + bottom: 60px; + left: 60px; + right: 60px; +} diff --git a/app/l8pr/static/containers/StripHeader/index.js b/app/l8pr/static/containers/StripHeader/index.js new file mode 100644 index 0000000..72952af --- /dev/null +++ b/app/l8pr/static/containers/StripHeader/index.js @@ -0,0 +1,36 @@ +import React from 'react' +import { connect } from 'react-redux' +import * as selectors from '../../selectors' +import { toggleStrip } from '../../actions/browser' +import { get } from 'lodash' +import './style.scss' + +function StripHeaderComponent({ trackTitle, stripOpened, toggleStrip, showTitle, onLogin }) { + return ( +
+ + {trackTitle}
+ {showTitle} +
+
+ + {!stripOpened && 'keyboard_arrow_up'} + {stripOpened && 'keyboard_arrow_down'} + + account_circle +
+
+ ) +} + +const mapStateToProps = (state) => ({ + trackTitle: get(selectors.getCurrentTrack(state), 'title'), + showTitle: get(selectors.getCurrentShow(state), 'title'), + stripOpened: state.browser.stripOpened, +}) + +const mapDispatchToProps = (dispatch) => ({ + toggleStrip: () => (dispatch(toggleStrip())), + onLogin: () => (dispatch(login())), +}) +export default connect(mapStateToProps, mapDispatchToProps)(StripHeaderComponent) diff --git a/app/l8pr/static/containers/StripHeader/style.scss b/app/l8pr/static/containers/StripHeader/style.scss new file mode 100644 index 0000000..d4a8d35 --- /dev/null +++ b/app/l8pr/static/containers/StripHeader/style.scss @@ -0,0 +1,12 @@ +.StripHeader { + background-color: white; +} +.StripHeader__title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: normal; + padding-top: 18px; + font-weight: 500; + text-align: left; +} diff --git a/app/l8pr/static/containers/index.js b/app/l8pr/static/containers/index.js new file mode 100644 index 0000000..fa63771 --- /dev/null +++ b/app/l8pr/static/containers/index.js @@ -0,0 +1,7 @@ +export HomeView from './Home/index'; +export LoginView from './Login/index'; +export ProtectedView from './Protected/index'; +export NotFoundView from './NotFound/index'; +export Strip from './Strip/index'; +export StripHeader from './StripHeader/index' +export Controller from './Controller/index' diff --git a/app/l8pr/static/index.html b/app/l8pr/static/index.html new file mode 100644 index 0000000..50a3aac --- /dev/null +++ b/app/l8pr/static/index.html @@ -0,0 +1,31 @@ + + + + + + + Loopr.tv + + + + + + + + + + + + + +
+ + + diff --git a/app/l8pr/static/index.js b/app/l8pr/static/index.js new file mode 100644 index 0000000..98180b4 --- /dev/null +++ b/app/l8pr/static/index.js @@ -0,0 +1,33 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { browserHistory } from 'react-router'; +import { syncHistoryWithStore } from 'react-router-redux'; + +import Root from './containers/Root/Root'; +import configureStore from './store/configureStore'; +import { authLoginUserSuccess } from './actions/auth'; + + +const initialState = {}; +const target = document.getElementById('root'); + +const store = configureStore(initialState, browserHistory); +const history = syncHistoryWithStore(browserHistory, store); + +const node = ( + +); + +const token = sessionStorage.getItem('token'); +let user = {}; +try { + user = JSON.parse(sessionStorage.getItem('user')); +} catch (e) { + // Failed to parse +} + +if (token !== null) { + store.dispatch(authLoginUserSuccess(token, user)); +} + +ReactDOM.render(node, target); diff --git a/app/l8pr/static/login/module.js b/app/l8pr/static/login/module.js deleted file mode 100644 index 884dda4..0000000 --- a/app/l8pr/static/login/module.js +++ /dev/null @@ -1,92 +0,0 @@ -(function() { -'use strict'; - -ModalInstanceCtrl.$inject = ['login', '$http', '$uibModalInstance', 'Api']; -function ModalInstanceCtrl(login, $http, $uibModalInstance, Api) { - var vm = this; - angular.extend(vm, { - cancel: function() { - $uibModalInstance.dismiss('cancel'); - }, - recover: function() { - Api.Register.one('password').one('reset').customPOST({ - email: vm.email - }).then(function onSuccess(d) { - $uibModalInstance.close(); - }, function onError(e) { - vm.recoverErrors = e.data; - }); - }, - create: function() { - vm.registerErrors = undefined; - if (vm.registerForm.$invalid) { - return; - } - Api.Register.one('register').customPOST({ - password: vm.password, - email: vm.email, - username: vm.username - }).then(function onSuccess(d) { - login.loginWithCred(vm.username, vm.password).then($uibModalInstance.close); - }, function onError(e) { - vm.registerErrors = e.data; - }); - }, - login: function() { - vm.loginError = false; - login.loginWithCred(vm.username, vm.password).then($uibModalInstance.close, function error(err) { - vm.loginError = true; - }); - } - }); -} - -angular.module('loopr.login', ['loopr.api']) -.config(['$httpProvider', function($httpProvider) { - $httpProvider.defaults.xsrfCookieName = 'csrftoken'; - $httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken'; -}]) -.service('login', ['Api', '$rootScope', '$uibModal', '$http', '$q', -function(Api, $rootScope, $uibModal, $http, $q) { - $rootScope.user = {}; - var service = { - currentUser: {}, - logout: function() { - service.currentUser = {}; - return Api.Auth.one('logout').get(); - }, - loginWithCred: function(username, password) { - return $q(function(resolve, reject) { - $http.post('api-auth/login/', { - username: username, - password: password - }, { - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, - transformRequest: function(data){ - return $.param(data); - } - }).success(function(responseData) { - resolve(service.login()); - }); - }); - }, - login: function() { - return Api.Accounts.one('me').get().then(function(currentUser) { - $rootScope.currentUser = currentUser; - service.currentUser = currentUser; - return currentUser; - }); - }, - openLoginView: function() { - var modalInstance = $uibModal.open({ - animation: true, - templateUrl: '/login/template.html', - controller: ModalInstanceCtrl, - controllerAs: 'vm' - }); - return modalInstance.result; - } - }; - return service; -}]); -})(); diff --git a/app/l8pr/static/login/template.html b/app/l8pr/static/login/template.html deleted file mode 100644 index 89f7073..0000000 --- a/app/l8pr/static/login/template.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/l8pr/static/main.html b/app/l8pr/static/main.html deleted file mode 100644 index 67274c8..0000000 --- a/app/l8pr/static/main.html +++ /dev/null @@ -1,116 +0,0 @@ - - -
- - -
- - - - - - -
-
-
-
-
-
-
-
-
- -
-
- first_page - chevron_left - - {{ vm.Player.currentStatus === 'playing' ? 'pause' : 'play_arrow' }} - - chevron_right - last_page - - {{ vm.Player.isMuted ? 'volume_off' : 'volume_up' }} - -
-
- - {{ vm.stripService.isAutoHideEnabled === true ? 'video_label' : 'call_to_action' }} - - - fullscreen - -
-
- help_outline -
-
- keyboard_arrow_down -
-
-
-
- -
- keyboard_arrow_up -
-
- -
-
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
diff --git a/app/l8pr/static/main.js b/app/l8pr/static/main.js deleted file mode 100644 index 7b6a0a1..0000000 --- a/app/l8pr/static/main.js +++ /dev/null @@ -1,328 +0,0 @@ -(function() { - 'use strict'; - - angular.module('loopr.player', [ - 'loopr.api', - 'loopr.strip', - 'loopr.login', - 'ui.bootstrap', - 'loopr.player.vimeo', - 'angular-confirm', - 'cfp.hotkeys', - 'loopr.player.youtube', - 'loopr.player.webtorrent', - 'FBAngular', - 'ui.router']) - .config(['$stateProvider', '$urlRouterProvider', 'hotkeysProvider', '$locationProvider', - function($stateProvider, $urlRouterProvider, hotkeysProvider, $locationProvider) { - hotkeysProvider.useNgRoute = false; - $locationProvider.html5Mode({enabled:true}).hashPrefix('#'); - $stateProvider - .state('resetPassword', { - url: '/resetpassword?uid&token', - controllerAs: 'vm', - controller: 'PlayerCtrl', - resolve: { - loop: ['Player', function(Player) { - return Player.loadLoop('discover') - .then(function(loop) { - return Player.playLoop(loop); - }); - }] - }, - templateUrl: '/main.html', - onEnter: ['$state', 'resetPassword', - function($state, resetPassword) { - resetPassword.open().result['finally'](function() { - $state.go('index'); - }); - }] - }) - .state('open', { - reloadOnSearch: false, - url: '/open/{q:.*}', - controller: ['$stateParams', 'Player', 'login', '$state', '$q', 'Api', - function($stateParams, Player, login, $state, $q, Api) { - function loadLoopAndComplete(username, persist) { - return Player.loadLoop(username, $stateParams.item) - .then(function(loop) { - $q.when((function getUpdatedInboxShow() { - if (persist) { - return $q.all([ - Api.FindOrCreateItem({url: $stateParams.q}), - Api.FindOrCreateInbox({user: loop.user}) - ]).then(function(results) { - var item = results[0], show = results[1]; - show.items.unshift(item); - show.put(); - _.remove(loop.shows_list, function(s) { - return s.id === show.id; - }); - return show; - }); - } else { - return Api.GetItemMetadata.one().get({url: $stateParams.q}) - .then(function(item) { - return {title: 'Open', items: [item]}; - }); - } - })()).then(function(show) { - loop.shows_list.unshift(show); - Player.setLoop(loop); - $state.go('index'); - }); - }); - } - return login.login().then(function(user) { - return loadLoopAndComplete(user.username, true); - }, function() { - return loadLoopAndComplete('discover', false); - }); - }] - }) - .state('index', { - reloadOnSearch: false, - url: '/:username?show&item', - controller: 'PlayerCtrl', - templateUrl: '/main.html', - controllerAs: 'vm', - resolve: { - loop: ['$stateParams', 'Player', 'login', '$state', '$q', - function($stateParams, Player, login, $state, $q) { - if (!angular.isDefined($stateParams.username) || $stateParams.username === '' || $stateParams.username === '_=_') { - return login.login().then(function(user) { - $state.go('index', {username: user.username}); - }, function() { - $state.go('index', {username: 'discover'}); - }); - } - if (angular.isDefined(Player.loop)) { - Player.playShow(Player.loop.shows_list[0], 0); - return Player.loop; - } - var username = $stateParams.username; - if ($stateParams.username === 'resetpassword') { - username = 'discover'; - } - return Player.loadLoop(username, $stateParams.item) - .then(function(loop) { - return Player.playLoop(loop, $stateParams.show, $stateParams.item); - }); - }] - } - }) - .state('index.open', { - reloadOnSearch: false, - params: { - loopToExplore: null - }, - 'abstract': true, - resolve: { - loopToExplore: ['$stateParams', 'Player', 'loop', - function($stateParams, Player, loop) { - return Player.loadLoop($stateParams.loopToExplore || loop); - }], - latestItemsShow: ['Api', function(Api) { - return Api.LatestItems().then(function(items) { - return {title: 'What\'s new in loopr.tv', items: items, show_type: 'last_item'}; - }); - }] - }, - views: { - header: { - controller: 'StripHeaderCtrl', - templateUrl: '/strip/header/template.html', - controllerAs: 'vm' - }, - body: { - template: '
' - } - } - }) - .state('index.open.loop', { - reloadOnSearch: false, - url: '/loop/:loopToExplore', - views: { - body: { - controller: 'LoopExplorerCtrl', - templateUrl: '/strip/loop/template.html', - controllerAs: 'vm' - } - } - }) - .state('index.open.show', { - reloadOnSearch: false, - url: '/show/:showToExploreId', - params: { - showToExplore: null - }, - views: { - body: { - controller: 'ShowExplorerCtrl', - templateUrl: '/strip/show/template.html', - controllerAs: 'vm', - resolve: { - show: ['$stateParams', 'Api', 'loop', 'ApiCache', - function($stateParams, Api, loop, ApiCache) { - // if a show object is given, open it and update the params - if ($stateParams.showToExplore) { - $stateParams.showToExploreId = $stateParams.showToExplore.id; - if (ApiCache.isDirty) { - ApiCache.isDirty = false; - return $stateParams.showToExplore.get(); - } else { - return $stateParams.showToExplore; - } - } - // if the show is in the current loop, open it (and keep items order) - var show = _.find(loop.shows_list, function(show) { - return show.id === parseInt($stateParams.showToExploreId, 10); - }); - if(show) { - if (ApiCache.isDirty) { - ApiCache.isDirty = false; - return show.get(); - } else { - return show; - } - } - // otherwise, load from API - return Api.Shows.one($stateParams.showToExploreId).get(); - }] - } - } - } - }) - .state('index.open.latest', { - url: '/latest', - reloadOnSearch: false, - views: { - body: { - controller: 'SearchCtrl', - templateUrl: '/strip/search-results/template.html', - controllerAs: 'vm', - resolve: { - query: function() {return 'latest';}, - results: ['Api', function(Api) { - return Api.LatestItems(); - }] - } - } - } - }) - .state('index.open.search', { - url: '/search/{q:.*}', - reloadOnSearch: false, - views: { - body: { - controller: 'SearchCtrl', - templateUrl: '/strip/search-results/template.html', - controllerAs: 'vm', - resolve: { - query: function($stateParams) { - return $stateParams.q; - }, - results: ['Api', 'query', function(Api, query) { - if (query) { - var urlRegex = /(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/; - if (urlRegex.test(query)) { - return Api.GetItemMetadata.one().get({url: query}).then(function(item) { - return [item]; - }); - } else { - return Api.Search.getList({'title': query}); - } - } - return []; - }] - } - } - } - }); - }]) - .service('ApiCache', [function() { - var self = this; - angular.extend(self, { - isDirty: false - }); - }]) - .service('$history', ['$state', '$rootScope', '$window', function($state, $rootScope, $window) { - var history = []; - var self = this; - angular.extend(self, { - push: function(state, params) { - if (history.length > 0 && state.name === history[history.length - 1].state.name) { - // if last state has the same name, update it - history[history.length - 1].params = params; - } else { - // if not, push a new state - history.push({ state: state, params: params }); - } - }, - // used in $stateChangeSuccess handler to know if change comes from back function - goingBack: false, - back: function(fallback) { - var prev = history.pop(); - self.goingBack = true; - if (angular.isDefined(prev)) { - return $state.go(prev.state, prev.params); - } else { - if (fallback) { - $state.go(fallback); - } else { - $state.go('index'); - } - } - } - }); - }]) - .run(['$history', '$state', '$rootScope', 'hotkeys', '$timeout', - function($history, $state, $rootScope, hotkeys, $timeout) { - $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){ - $timeout(function() { - $('[ui-view="body"]').addClass('spinner'); - }, 0, false); - }); - - $rootScope.$on('$stateChangeSuccess',function(event, toState, toParams, fromState, fromParams){ - $timeout(function() { - $('[ui-view="body"]').removeClass('spinner'); - }, 0, false); - }); - $rootScope.$on('$stateChangeSuccess', function(event, to, toParams, from, fromParams) { - if ($history.goingBack) { - $history.goingBack = false; - return; - } - if (!from['abstract'] && !_.contains(['index', 'open', 'resetPassword'], from.name)) { - delete fromParams.show; - delete fromParams.item; - $history.push(from, fromParams); - } - }); - hotkeys.add({ - combo: ['ctrl+f'], - description: 'Search', - callback: function(e) { - e.preventDefault(); - $state.go('index.open.search').then(function(s) { - $rootScope.$emit('openSearch'); - }); - } - }); - }]) - .filter('seconds', function() { - return function(time) { - if (angular.isDefined(time) && angular.isNumber(time) && !isNaN(time)) { - var hours = Math.floor(time / 3600); - time = time - hours * 3600; - var minutes = Math.floor(time / 60); - var time_str = ''; - if (hours > 0) { - time_str += hours + 'h'; - } - return time_str + minutes + 'm'; - } - }; - }); -})(); diff --git a/app/l8pr/static/main.less b/app/l8pr/static/main.less deleted file mode 100644 index 4e07406..0000000 --- a/app/l8pr/static/main.less +++ /dev/null @@ -1,622 +0,0 @@ -@import "bower_components/bootstrap/less/bootstrap.less"; -@import "bower_components/plyr/src/less/plyr.less"; -@import "bower_components/components-font-awesome/less/font-awesome.less"; -@fa-font-path: "/static/bower_components/components-font-awesome/fonts"; -@import "variables.less"; -@import "strip/style.less"; -@import "strip/show/style.less"; -@import "/player/webtorrent/style.less"; -@import "strip/loop/style.less"; -@import "strip/search-results/style.less"; -@import "strip/header/style.less"; -@import "strip/help/style.less"; -@import "strip/feed/style.less"; - -a, [ng-click], [ui-sref], [uib-dropdown-toggle] { - //color: @link-color; - text-decoration: none; - cursor: pointer; - &:hover { - text-decoration: @link-hover-decoration; - color: @text-color; // @link-hover-color; - } -} - -html, body, div[ng-view] { - height: 100%; - overflow: hidden; - margin: 0; - background-color: #1A1A1A; - transform: translateZ(0); /* activation de l'AM */ -} -iframe { - position: absolute; - z-index: 5; -} -// youtube player -body .plyr { - position: absolute; - top: 0; - right: 0; - left: 0; - bottom: 0; - .plyr__video-embed, - .plyr__video-wrapper, - .plyr__video-wrapper video { - height: 100%; - } -} -.loopr-giphy { - position: absolute; - top: 0; - bottom: 0; - right: 0; - left: 0; - .background { - position: absolute; - z-index: 0; - top: 0; - bottom: 0; - right: 0; - left: 0; - background-size: cover; - background-repeat: no-repeat; - background-position: center center; - } - &.repeat { - .repeatable { - position: absolute; - z-index: 2; - top: 0; - bottom: 0; - right: 0; - left: 0; - background-repeat: repeat; - } - } - &.symmetry { - img {width: 50%;} - img:nth-child(1) { - float: left; - } - img:nth-child(2) { - float: right; - -moz-transform: scaleX(-1); - -o-transform: scaleX(-1); - -webkit-transform: scaleX(-1); - transform: scaleX(-1); - filter: FlipH; - -ms-filter: "FlipH"; - } - } - img { - position: relative; - z-index: 2; - display: block; - margin: 0 auto; - width: 100%; - height: 100%; - } -} -.overlay { - position: absolute; - z-index: 9; - top: 0; - bottom: 0; - right: 0; - left: 0; -} -.strip { - margin: @MARGE_OUTER; - position: absolute; - z-index: 10; - bottom: 0px; - left: 0; - right: 0; - opacity: 1; - transition-property: opacity; - transition-property: height; - transition-duration: 2s; - &.hidden { - opacity: 0; - } - @media (max-width: @screen-xs){ - margin: @MARGE_OUTER--mobile; - } -} - -// -// BOOTSTRAP OVERRIDE -// - -.col-xs-1, .col-xs-2, .col-xs-4, .col-xs-6, .col-xs-8, .col-xs-10, .col-xs-11, .col-xs-12, -.col-sm-8, .col-sm-4, .col-sm-12, -.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, -.col-md-push-9, .col-md-pull-3 { - padding-left: 0px !important; - padding-right: 0px !important; -} - -.row { - margin-left: 0px !important; - margin-right: 0px !important; -} - -.list-group { - margin-bottom: 0px; -} - -.list-group-item { - border: 1px #fff; - background-color: transparent; - padding-left: 0px; - margin-left: 0px; -} - -.list-group-item.active, -.list-group-item.active:hover, -.list-group-item.active:focus { - height: 180px; - background-color: @body-bg !important; - color: @text-color !important; - font-size: 5em !important; -} - -.container-fluid { - padding-left: 0px; - padding-right: 0px; -} - -.progress { - height: 5px !important; - transition-property: height; - transition-duration: .20s; - &:hover { - height: 15px !important; - } -} - -.navbar-collapse { - padding: 0px; - margin: 0px; -} - - -// modals.less - -.modal { -} - -.modal-content { - background-color: @body-bg; - .box-shadow(10px 10px 0px fade(@text-color, 20)); - font-family: @FONT2; - border: none !important; - padding: 30px; -} - -.modal-backdrop { - background-color: fade(@brand-primary, 20); - &.fade { .opacity(0); } - &.in { .opacity(@modal-backdrop-opacity); } -} - -.modal-header { - border: none; - .row; -} -.modal-title { - .h3; - .row; - padding-bottom: 0px; -} - -.modal-body { -} -.modal-footer { - border: none; -} -.form-control { - &:not(.input-lg) { - height: 35px; - } - &.ng-invalid.ng-dirty { - .has-error; - .form-control; - } -} - -//panels - -.panel { - //margin-bottom: @line-height-computed; - //background-color: @panel-bg; - border: 0px !important; - //border-radius: @panel-border-radius; - .box-shadow(0 0px 0px rgba(0,0,0,0)); - background-color: transparent; - -} - - -.panel-heading, .panel-default { - padding: 10 0 10 0; - border: 0px !important; - background-color: transparent;; - //.border-top-radius((@panel-border-radius - 1)); - - > .dropdown .dropdown-toggle { - color: inherit; - } -} -.panel-default > .panel-heading { - border: 0px !important; - background-color: transparent;; -} -.panel-body { - border: 0px !important; - padding: @panel-body-padding; - &:extend(.clearfix all); - background-color: transparent;; - -} - -.panel-title { -padding: 10 0 10 0; -border: 0px !important; -background-color: transparent; -font-family: @FONT1 !important; -} -// END Override Bootstrap // -// - -// Material icons -.material-icons { - font-size: 22px !important; -} - - -// List *Ultimate* // Boom. -.list { - font-family: @FONT2; - .list-group; - &__item { - a { - font-weight: bold; - } - &:nth-child(odd) { - background-color: @color-list-odd; - } - &:nth-child(even) { - background-color: @color-list-even; - } - .list-group-item; - .row; - //.text-right; - overflow: hidden; - margin-bottom: 0; - padding: 0px; - //height: 100px; - transition-property: height; - transition-duration: .25s; - box-shadow: 0px -10px 20px fade(@text-color, 5); - - @media (min-width: @screen-md){ - height: 120px; - } - &.list__item--active { - background-color: @body-bg; - height: auto; - @media (min-width: @screen-md){ - } - .list__item__title { - .h6; - font-weight: 700; - @media (min-width: @screen-md){ - .h4; - } - } - } - - &.list__item--playing, - &.list__item--playing:hover { - .list__item--active ; - box-shadow: 0px -10px 20px fade(@text-color, 0); - //box-shadow: 0px -10px 15px fade(@text-color, 4), - // inset 0px -10px 15px fade(@text-color, 4); - .list__item__title { - .h5; - line-height: 1.4em !important; - font-weight: 700; - padding-bottom: 10px; - @media (min-width: @screen-md){ - .h3; - line-height: 1.5em !important; - font-weight: 700; - padding-bottom: 10px; - } - } - .list__item__info { - padding-bottom: 40px; - //background-color: yellow; - - } - } - &__title { - .list-group-item-heading; - .h6; - line-height: 1.6em !important; - padding-left: 20px; - padding-top: 10px; - @media (min-width: @screen-md){ - .h5; - padding-left: 40px; - padding-top: 30px; - padding-right: 30px; - } - } - .item {} //Default type - .show { - font-family: @FONT1; - font-size: 110%; - margin-bottom: 5px; - letter-spacing: .025em; - } - - &__info { - .list-group-item-text; - .h6; - padding-left: 20px; - @media (min-width: @screen-md){ - .h5; - padding-left: 40px; - padding-right: 30px; - padding-bottom: 10px; - font-weight: 300 !important; - } - a { - //color: @text-color; - } - i { - font-size: 80%; - } - span { - em { - font-style: normal; - font-weight: 500; - } - } - } - &__actions { - padding-top: 30px; - padding-right: 20px !important; - @media (min-width: @screen-md){ - padding-top: 30px; - padding-right: 30px !important; - } - .text-right; - a { - vertical-align: top; - } - } - &__illu { - img { - width: 100%; - @media (min-width: @screen-md) { - .img-responsive; - } - } - } - &:hover { - a, [ng-click] {color: @text-color;} - background-color: @body-bg; - .fa-youtube-play { - color: @color_youtube; - } - .fa-soundcloud { - color: @color_soundcloud; - } - .fa-vimeo-square { - color: @color_vimeo; - } - .list__item__title { - font-weight: bold; - } - .list-group-item-text { - opacity: 1; - font-weight: 300; - } - .list__item__illu:after { - position: absolute; - top: 0px; - right: 0px; - left: 0px; - bottom: 0px; - content: 'play_arrow'; - font-family: 'Material Icons'; - -webkit-font-feature-settings: 'liga'; - background-color: fade(@brand-primary, 15); - background-blend-mode: multiply !important; - color: @body-bg; - font-size: 2.5em; - padding-top: 20px; - text-align: center; - text-shadow: 0px 0px 15px @text-color; - @media (min-width: @screen-md){ - top: 0px; - right: 0px; - left: 0px; - bottom: 0px; - padding-top: 40px; - } - } - } - } -} - - -// Cover Ultimate -.cover { - .row; - font-family: @FONT1; - color: @text-color; - background-color: @body-bg; - box-sizing: padding-box; - padding: (@MARGE * 2) (@MARGE * 3); - @media (max-width: @screen-sm){ - padding: (@MARGE * 2) (@MARGE * 2); - } - box-shadow: 0px 10px 0px @SHADOW; - position: relative; - z-index: 2; - &__title { - font-size: 150%; - } - &__details { - font-family: @FONT2; - } - &__actions { - text-align: right; - &:hover{color:blue;} - } -} - -//Side bar -.side-bar { - .row; - .text-left; - background-blend-mode: multiply; - font-family: @FONT2; - color: @text-color; - margin: 0 !important; - overflow: hidden; - - a, [ng-click], [ui-sref] { - color: fade(@text-color, 80) !important; - &:hover { - color: fade(@brand-primary, 100) !important; - //background-color: @body-bg; - } - } - &__title { - .h4; - margin: 0px; - padding: 20px 20px 10px 20px; - font-weight: 500; - background-color: @body-bg; - @media (min-width: @screen-md){ - .h2; - padding: 30px; - line-height: 1.5em; - margin: 0px; - } - } - .show { - font-family: @FONT1; - font-weight: 700; - } - .results { - background-color: @text-color !important; - color: @body-bg !important; - } - - &__info { - .h6; - .row; - margin: 0px; - padding: 0px 30px 20px 20px !important; - background-color: @body-bg; - @media (min-width: @screen-md){ - .h5; - margin: 0px; - padding: 0px 30px 30px 30px !important; - line-height: 1.5em; - } - } - &__filter { - .h5; - padding: 10px 20px; - color: fade(@text-color, 90) !important; - margin: 0px; - border: none !important; - i { - font-size: 18px !important; - } - @media (min-width: @screen-md){ - //.text-left; - .h5; - padding: 20px 30px 50px 30px; - line-height: 1.5em; - margin: 0px; - i { - font-size: 24px !important; - } - } - &__section { - margin-top: 20px; - font-weight: 300; - display: inline-block; - .title { - font-style: italic; - display: inline-block; - } - .note-btn { - .text-right; - //float: right; - display: inline-block; - &:hover, a { - color: @body-bg; - background-color: fade(@text-color,0); - } - } - .note { - .h6; - margin: 5px; - font-style: italic; - line-height: 1.6em; - } - } - } -} -// LOADER -@keyframes spinner { - to {transform: rotate(360deg);} -} - -@-webkit-keyframes spinner { - to {-webkit-transform: rotate(360deg);} -} - -.spinner { - min-width: 30px; - position: relative; - min-height: 30px; - &:before { - position: absolute; - z-index: 9; - background-color: rgba(0,0,0,.2); - top: 0; - bottom: 0; - left: 0; - right: 0; - content: ''; - } - &:after { - content: 'Loading…'; - position: absolute; - top: 50%; - left: 50%; - width: 40px; - height: 40px; - margin-top: -13px; - margin-left: -13px; - } - &:not(:required):after { - content: ''; - border-radius: 50%; - border-top: 4px solid black; - border-right: 4px solid transparent; - animation: spinner .6s linear infinite; - -webkit-animation: spinner .6s linear infinite; - } -} diff --git a/app/l8pr/static/player/giphy.js b/app/l8pr/static/player/giphy.js deleted file mode 100644 index 56b38d4..0000000 --- a/app/l8pr/static/player/giphy.js +++ /dev/null @@ -1,58 +0,0 @@ -(function() { - 'use strict'; - angular.module('loopr.player').directive('looprGiphy', - ['Player', '$timeout', '$http', - function(Player, $timeout, $http) { - return { - restrict: 'E', - template: [ - '
', - '
', - '
', - '', - '', - '
', - '
', - '
' - ].join(''), - link: function(scope, elmt) { - var gifTimeout; - var layoutTimeout; - var layouts = ['default', 'symmetry', 'repeat']; - var giphy_keywords = Player.currentShow.settings && - Player.currentShow.settings.giphy_tags && - Player.currentShow.settings.giphy_tags.split(',') || []; - var giphy_url = '//api.giphy.com/v1/gifs/random?rating=r&api_key=dc6zaTOxFJmzC&tag='; - angular.extend(scope, { - background: '/static/images/recordPlayer.gif' - }); - function updateGif() { - function updateLayout() { - $timeout.cancel(layoutTimeout); - scope.layout = layouts[Math.floor(Math.random()*layouts.length)]; - layoutTimeout = $timeout(updateLayout, 3000, false); - } - $timeout.cancel(gifTimeout); - scope.keyword = giphy_keywords[(giphy_keywords.indexOf(scope.keyword) + 1) % giphy_keywords.length]; - $http.get(giphy_url + scope.keyword).then(function(data) { - var image_url = data.data.data.image_original_url.replace('http://', '//'); - $('') - .attr('src', image_url) - .on('load', function() { - scope.soundcloudArtwork = image_url; - if (Player.currentShow.settings && Player.currentShow.settings.dj_layout) { - updateLayout(); - } - gifTimeout = $timeout(updateGif, 10000, false); - this.remove(); - }); - }); - } - updateGif(); - } - }; - }]); -})(); diff --git a/app/l8pr/static/player/player.ctrl.js b/app/l8pr/static/player/player.ctrl.js deleted file mode 100644 index 68a1d49..0000000 --- a/app/l8pr/static/player/player.ctrl.js +++ /dev/null @@ -1,146 +0,0 @@ -(function() { - 'use strict'; - - PlayerCtrl.$inject = ['Player', '$stateParams', '$timeout','login', 'loop', 'addToShowModal', 'Api', - '$rootScope', 'hotkeys', '$scope', '$q', 'Fullscreen', 'upperStrip', 'lowerStrip', 'strip', '$state', 'strip', 'help']; - function PlayerCtrl(Player, $stateParams, $timeout, login, loop, addToShowModal, Api, - $rootScope, hotkeys, $scope, $q, Fullscreen, upperStrip, lowerStrip, strip, $state, stripService, help) { - var vm = this; - angular.extend(vm, { - $state: $state, - strip: strip, - Player: Player, - showsCount: 0, - currentUser: login.currentUser, - addToShowModal: addToShowModal, - upperStrip: upperStrip, - lowerStrip: lowerStrip, - stripService: stripService, - previousShow: Player.previousShow, - previousItem: Player.previousItem, - nextItem: Player.nextItem, - nextShow: Player.nextShow, - playPause: Player.playPause, - help: function() { - console.log('coucou'); - help.open(); - }, - isExtented: function() { - return !_.contains(['index', 'resetPassword'], vm.$state.current.name); - }, - setPosition: function($event) { - return Player.setPosition(($event.offsetX / $event.currentTarget.offsetWidth) * 100); - }, - isFullScreen: Fullscreen.isEnabled, - toggleFullscreen: function() { - if (Fullscreen.isEnabled()) { - Fullscreen.cancel(); - } else { - Fullscreen.all(); - } - }, - login: login, - showAndHideStrip: _.throttle(strip.showAndHide, 500) - }); - function setBanner(item, show) { - var lines = [item.title]; - if (show) { - lines.push(['Show', ''+show.title+'', 'by', loop.username].join(' ')); - } - if (item.subtitle) { - lines.push(item.subtitle); - } - lines.push(item.title); - upperStrip.setBanner(lines); - // show strip - if (strip.isAutoHideEnabled) { - strip.showAndHide(); - } - } - setBanner(vm.Player.currentItem, vm.Player.currentShow); - $scope.$on('player.play', function ($event, item, show) { - setBanner(item, show); - }); - // HOTKEYS - hotkeys.bindTo($scope) - .add({ - combo: ['c'], - description: 'Show the controller', - callback: strip.toggleController - }) - .add({ - combo: 'm', - description: 'Mute/Unmute', - callback: vm.Player.toggleMute - }) - .add({ - combo: 'space', - description: 'pause/play', - callback: vm.Player.playPause - }) - .add({ - combo: 'f', - description: 'Full screen', - callback: function() { - if (Fullscreen.isEnabled()) { - Fullscreen.cancel(); - } else { - Fullscreen.all(); - } - } - }) - .add({ - combo: 'right', - description: 'next show', - callback: function(e) { - vm.Player.nextShow(); - e.preventDefault(); - } - }) - .add({ - combo: 'left', - description: 'previous show', - callback: function(e) { - vm.Player.previousShow(); - e.preventDefault(); - } - }) - .add({ - combo: 'up', - description: 'previous item', - callback: vm.Player.previousItem - }) - .add({ - combo: 'down', - description: 'next item', - callback: vm.Player.nextItem - }); - } - - angular.module('loopr.player') - .controller('PlayerCtrl', PlayerCtrl) - .directive('autoHideCursor', ['$timeout', function($timeout) { - return { - scope: true, - link: function(scope, element) { - var hideCursorTimeout; - function hideCursor() { - element.css('cursor', 'none'); - } - function showCursor() { - element.css('cursor', ''); - } - $timeout(hideCursor, 3000, false); - element.on('mouseout', function() { - $timeout.cancel(hideCursorTimeout); - showCursor(); - }); - element.on('mousemove', function() { - $timeout.cancel(hideCursorTimeout); - showCursor(); - hideCursorTimeout = $timeout(hideCursor, 3000, false); - }); - } - }; - }]); -})(); diff --git a/app/l8pr/static/player/player.service.js b/app/l8pr/static/player/player.service.js deleted file mode 100644 index 3fa5745..0000000 --- a/app/l8pr/static/player/player.service.js +++ /dev/null @@ -1,266 +0,0 @@ -(function() { - 'use strict'; - - Player.$inject = ['$rootScope', 'localStorageService', '$location', '$state', - '$timeout', 'login', 'Api', '$q', 'ApiCache']; - function Player($rootScope, localStorageService, $location, $state, - $timeout, login, Api, $q, ApiCache) { - var self = this; - angular.extend(self, { - currentPosition: 0, - loop: undefined, - currentShow: undefined, - currentItem: undefined, - currentStatus: undefined, - isMuted: localStorageService.get('muted'), - setCurrentPosition: function(position) { - self.currentPosition = position; - }, - setPosition: function(percent) { - $rootScope.$broadcast('player.seekTo', percent); - }, - getStatus: function() { - return self.currentStatus; - }, - setStatus: function(status) { - self.currentStatus = status; - }, - playPause: function() { - $rootScope.$broadcast('player.playPause'); - }, - setLoop: function(loop) { - self.loop = loop; - }, - playLoop: function(loop, selectedShow, selectedItem) { - // a show is asked - var show = _.find(loop.shows_list, function(show) { - if (show.id) { - return show.id.toString() === selectedShow; - } - }); - // a show is asked but not part of the loop, then we add it to the loop - if (!angular.isDefined(show) && selectedShow) { - show = Api.Shows.one(selectedShow).get().then(function(show) { - loop.shows_list.push(show); - return show; - }); - } - return $q.when(show).then(function(show) { - var item_index; - // an item is asked - if (show && selectedItem) { - item_index = _.findIndex(show.items, function(link) { - return parseInt(selectedItem, 10) === parseInt(link.id, 10); - }); - } - // item not found, play first one - if (item_index === -1) { - item_index = 0; - } - self.setLoop(loop); - self.playShow(show, item_index); - return loop; - }); - }, - loadLoop: function(usernameOrLoop, selectedItem) { - return $q.when((function() { - if (typeof(usernameOrLoop) === 'string') { - return Api.Loops.getList({'username': usernameOrLoop}).then(function(loops) { - var loop = loops[0]; - loop.username = usernameOrLoop; - return loop; - }); - } else { - if (ApiCache.isDirty) { - ApiCache.isDirty = false; - return usernameOrLoop.get().then(function(loop) { - return Api.Accounts.one(loop.user).get().then(function(user) { - loop.username = user.username; - return loop; - }); - }); - } else { - return usernameOrLoop; - } - - } - })()).then(function(loop) { - if (loop.$randomised) { - return loop; - } - // shuffle ? - function shuffle(o) { - for(var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); // jshint ignore:line - return o; - } - loop.shows_list.forEach(function(show) { - if (show.settings && show.settings.shuffle) { - var item_to_save; - if (angular.isDefined(selectedItem)) { - item_to_save = _.find(show.items, function(link) { - return link.id === selectedItem; - }); - } - shuffle(show.items); - if (angular.isDefined(item_to_save)) { - show.items.splice(0, 0, show.items.splice(show.items.indexOf(item_to_save), 1)[0]); - } - } - }); - loop.$randomised = true; - return loop; - }); - }, - toggleMute: function() { - self.isMuted = !self.isMuted; - $rootScope.$broadcast('player.toggleMute'); - localStorageService.set('muted', self.isMuted); - }, - playItem: function(item) { - var currentIndex = self.currentShow.items.indexOf(self.currentItem); - if (currentIndex === -1) { - console.error('item not found in', self.currentShow); - } - var show = angular.copy(self.currentShow); - show.items.splice(currentIndex + 1, 0, item); - self.playShow(show, currentIndex + 1); - }, - playShow: function(show, index) { - if (!angular.isDefined(show)) { - var now = new Date(), - then = new Date( - now.getFullYear(), - now.getMonth(), - now.getDate(), - 0,0,0), - diff = (now.getTime() - then.getTime()) / 1000; - var watch_at = diff % self.loop.duration(); - var spent = 0; - for (var i = 0; i < self.loop.shows_list.length; i++) { - if (angular.isDefined(index)) { - break; - } - show = self.loop.shows_list[i]; - if (spent < watch_at) { - if (spent + show.duration() < watch_at) { - spent += show.duration(); - } else { - for (var j = 0; j < show.items.length; j++) { - var link = show.items[j]; - if (spent + link.duration < watch_at) { - spent += link.duration; - } else { - index = j; - break; - } - } - } - } - } - } - index = index || 0; - self.currentShow = show; - self.currentItem = show.items[index]; - // deep linking - // $timeout(function() { - // if ($state.current.name.indexOf('index') > -1) { - // $location.search(angular.extend({}, $location.search(),{show: self.currentShow.id, item: self.currentItem.id})); - // } - // }, 250, false); - $timeout(function() { - $rootScope.$broadcast('player.play', self.currentItem, self.currentShow); - }); - }, - nextItem: function() { - var current_item_index = self.currentShow.items.indexOf(self.currentItem); - if (self.currentShow.items.length - 1 > current_item_index) { - // next item - self.playShow(self.currentShow, current_item_index + 1); - } else { - // next show - self.nextShow(); - } - }, - previousItem: function() { - var current_item_index = self.currentShow.items.indexOf(self.currentItem); - if (current_item_index - 1 > -1) { - // previous item - self.playShow(self.currentShow, current_item_index - 1); - } else { - // previous show and last item - self.previousShow(true); - } - }, - getNextShow: function() { - var current_index = self.loop.shows_list.indexOf(self.currentShow); - var next_show = 0; - if (current_index > -1 && current_index + 1 < self.loop.shows_list.length) { - next_show = current_index + 1; - } - return self.loop.shows_list[next_show]; - }, - nextShow: function() { - return self.playShow(self.getNextShow()); - }, - previousShow: function(last) { - last = last === true; - var current_index = self.loop.shows_list.indexOf(self.currentShow); - var previous_show = current_index - 1; - if (previous_show < 0) { - previous_show = self.loop.shows_list.length - 1; - } - var show = self.loop.shows_list[previous_show]; - var index = last ? show.items.length - 1 : 0; - return self.playShow(self.loop.shows_list[previous_show], index); - } - }); - - return self; - } - - function bannerService() { - var service = { - banner: [], - setBanner: function(banner) { - service.banner = banner; - }, - addQueries: function(queries) { - if (queries) { - var underlines = []; - queries.forEach(function(query) { - if (angular.isDefined(query.results) && angular.isDefined(query.results.items)) { - if (query.type === 'twitter') { - query.results.items.forEach(function(tweet) { - underlines.push(''); - }); - } - if (query.type === 'rss') { - query.results.items.forEach(function(rss) { - underlines.push('
'+query.results.title+' ' + - rss.title+ - ''); - }); - } - } - }); - service.setBanner(underlines); - } - } - }; - return service; - } - - angular.module('loopr.player') - .factory('Player', Player) - .factory('lowerStrip', function() { - return bannerService(); - }) - .factory('upperStrip', function() { - return bannerService(); - }); - -})(); diff --git a/app/l8pr/static/player/soundcloud/module.js b/app/l8pr/static/player/soundcloud/module.js deleted file mode 100644 index d509d1f..0000000 --- a/app/l8pr/static/player/soundcloud/module.js +++ /dev/null @@ -1,110 +0,0 @@ -(function() { - 'use strict'; - SoundcloudDirective.$inject = ['Player', '$interval', '$q', '$http']; - function SoundcloudDirective(Player, $interval, $q, $http) { - return { - scope: { - soundcloudItem: '=' - }, - restrict: 'E', - link: function(scope, element) { - var soundcloudPlayer; - var progressionTracker; - - scope.playPause = function() { - soundcloudPlayer.then(function(sound) { - if (sound.isPlaying()) { - pause(); - } else { - play(); - } - }); - }; - - function mute() { - soundcloudPlayer.then(function(sound) { - sound.setVolume(0); - }); - } - - function unmute() { - soundcloudPlayer.then(function(sound) { - sound.setVolume(1); - }); - } - - scope.toggleMute = function() { - soundcloudPlayer.then(function(sound) { - if (sound.getVolume() === 1) { - mute(); - } else { - unmute(); - } - }); - }; - - function clear() { - if (angular.isDefined(soundcloudPlayer)) { - soundcloudPlayer.then(function(sound) { - sound.pause(); - }); - soundcloudPlayer = null; - } - $interval.cancel(progressionTracker); - Player.setCurrentPosition(0); - } - - function pause() { - Player.setStatus('pause'); - soundcloudPlayer.then(function(sound) { - sound.pause(); - }); - } - - function play() { - Player.setStatus('playing'); - soundcloudPlayer.then(function(sound) { - sound.play(); - }); - } - - scope.$watch('soundcloudItem', function(n , o) { - clear(); - var soundDeferred = $q.defer(); - soundcloudPlayer = soundDeferred.promise; - SC.initialize({client_id: '847e61a8117730d6b30098cfb715608c'}); - SC.resolve(Player.currentItem.url).then(function(data) { - SC.stream(data.uri.replace('https://api.soundcloud.com', '')).then(function(player) { - play(); - soundDeferred.resolve(player); - if (Player.isMuted) { - mute(); - } - $interval.cancel(progressionTracker); - progressionTracker = $interval(function() { - if (player.streamInfo) { - Player.setCurrentPosition((player.currentTime() / player.streamInfo.duration) * 100); - if (player.currentTime() > player.streamInfo.duration - 5){ - Player.nextItem(); - } - } - }, 250); - }); - }); - }); - scope.$on('player.seekTo', function(e, percent) { - soundcloudPlayer.then(function(sound) { - sound.seek(Math.ceil((percent/100) * sound.streamInfo.duration)); - }); - }); - scope.$on('player.toggleMute', scope.toggleMute); - scope.$on('player.playPause', scope.playPause); - scope.$on('$destroy', function() { - clear(); - }); - } - }; - } - angular.module('loopr.player').directive('soundcloud', SoundcloudDirective); - -})(); diff --git a/app/l8pr/static/player/vimeo/module.js b/app/l8pr/static/player/vimeo/module.js deleted file mode 100644 index 20961ca..0000000 --- a/app/l8pr/static/player/vimeo/module.js +++ /dev/null @@ -1,76 +0,0 @@ -(function() { - 'use strict'; - VimeoDirective.$inject = ['Player', '$interval', '$rootScope']; - function VimeoDirective(Player, $interval, $rootScope) { - return { - scope: { - item: '=' - }, - restrict: 'E', - link: function(scope, element) { - var plyrPlayer = window.plyr.setup(element.get(0), { - autoplay: true, controls: [] - })[0]; - scope.$on('player.playPause', function() { - plyrPlayer.togglePlay(); - }); - scope.$on('player.toggleMute', function() { - plyrPlayer.setVolume((Player.isMuted) ? 0 : 100); - }); - scope.$on('player.seekTo', function(e, percent) { - plyrPlayer.embed.api('getDuration', function(duration) { - plyrPlayer.seek((percent/100) * duration); - }); - }); - scope.$watch('item', function() { - var url = scope.item.url.split('/'); - try { - plyrPlayer.source({ - type: 'video', - sources: [{ - src: url[url.length - 1], - type: 'vimeo' - }] - }); - // returns an error from a missing button. - } catch (e) {} - }); - var playerEvents = { - error: function(event) { - Player.setCurrentPosition(0); - Player.nextItem(); - }, - playing: function(event) { - Player.setStatus('playing'); - plyrPlayer.setVolume((Player.isMuted) ? 0 : 100); - plyrPlayer.embed.addEvent('playProgress', function(data) { - Player.setCurrentPosition(data.percent * 100); - }); - }, - pause: function(event) { - Player.setStatus('pause'); - }, - ended: function(event) { - Player.setCurrentPosition(0); - Player.nextItem(); - } - }; - angular.forEach(playerEvents, function(eventHandler, eventName) { - element.get(0).addEventListener(eventName, eventHandler); - }); - scope.$on('$destroy', function() { - angular.forEach(playerEvents, function(eventHandler, eventName) { - element.get(0).removeEventListener(eventName, eventHandler); - }); - plyrPlayer.destroy(); - }); - }, - template: [ - '
', - '
' - ].join('') - }; - } - angular.module('loopr.player.vimeo', []).directive('vimeo', VimeoDirective); - -})(); diff --git a/app/l8pr/static/player/webtorrent/module.js b/app/l8pr/static/player/webtorrent/module.js deleted file mode 100644 index e93d43b..0000000 --- a/app/l8pr/static/player/webtorrent/module.js +++ /dev/null @@ -1,100 +0,0 @@ -(function() { - 'use strict'; - WebtorrentDirective.$inject = ['Player', '$interval', '$rootScope', '$window']; - function WebtorrentDirective(Player, $interval, $rootScope, $window) { - return { - scope: { - item: '=' - }, - restrict: 'E', - link: function(scope, element) { - var client, player, interval; - var playerEvents = { - error: function(event) { - Player.setCurrentPosition(0); - Player.nextItem(); - }, - playing: function(event) { - Player.setStatus('playing'); - Player.volume = (player.muted) ? 0 : 1; - }, - timeupdate: function(event) { - Player.setCurrentPosition((player.currentTime / player.duration) * 100); - }, - pause: function(event) { - Player.setStatus('pause'); - }, - ended: function(event) { - Player.setCurrentPosition(0); - Player.nextItem(); - } - }; - scope.$watch('item', function() { - init(); - }); - function init() { - $interval.cancel(interval); - if (client) { - client.destroy(); - angular.extend(scope, { - torrent: {} - }); - client = null; - } - if (player) { - angular.forEach(playerEvents, function(eventHandler, eventName) { - player.removeEventListener(eventName, eventHandler); - }); - player = null; - } - element.find('video').remove(); - client = new $window.WebTorrent(); - client.add(scope.item.url, function (torrent) { - interval = $interval(function() { - angular.extend(scope, { - torrent: torrent - // progress: torrent.progress - }); - }, 1000); - var file = torrent.files[0]; - file.appendTo(element.get(0), function ready(err) { - player = element.find('video').get(0); - // var progressionTracker; - scope.$on('player.playPause', function() { - if (player.paused) { - player.play(); - } else { - player.pause(); - } - }); - scope.$on('player.seekTo', function(e, percent) { - player.currentTime = (percent/100) * player.duration; - }); - scope.$on('player.toggleMute', function() { - player.volume = (Player.isMuted) ? 0 : 1; - }); - angular.forEach(playerEvents, function(eventHandler, eventName) { - player.addEventListener(eventName, eventHandler); - }); - scope.$on('$destroy', function() { - angular.forEach(playerEvents, function(eventHandler, eventName) { - player.removeEventListener(eventName, eventHandler); - }); - }); - }); - }); - } - }, - template: [ - '
', - '
Progress: {{ torrent.progress * 100 }}%
', - '
downloadSpeed: {{ torrent.downloadSpeed / 1024 }} Kbytes/sec
', - '
numPeers: {{ torrent.numPeers }}
', - '
timeRemaining: {{ torrent.timeRemaining / 1000 | seconds }}
', - '
' - ].join('') - }; - } - angular.module('loopr.player.webtorrent', []).directive('webtorrent', WebtorrentDirective); - -})(); diff --git a/app/l8pr/static/player/webtorrent/style.less b/app/l8pr/static/player/webtorrent/style.less deleted file mode 100644 index 712c800..0000000 --- a/app/l8pr/static/player/webtorrent/style.less +++ /dev/null @@ -1,17 +0,0 @@ -.webtorrent { - &__info { - position: absolute; - background-color: black; - color: white; - width: 360px; - } - video { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - width: 100%; - height: 100%; - } -} diff --git a/app/l8pr/static/player/youtube/module.js b/app/l8pr/static/player/youtube/module.js deleted file mode 100644 index 800e25a..0000000 --- a/app/l8pr/static/player/youtube/module.js +++ /dev/null @@ -1,86 +0,0 @@ -(function() { - 'use strict'; - YoutubeDirective.$inject = ['Player', '$interval', '$rootScope']; - function YoutubeDirective(Player, $interval, $rootScope) { - return { - scope: { - item: '=' - }, - restrict: 'E', - link: function(scope, element) { - var plyrPlayer = window.plyr.setup(element.get(0), { - autoplay: true, controls: [] - })[0]; - scope.$on('player.playPause', function() { - plyrPlayer.togglePlay(); - }); - scope.$on('player.toggleMute', function() { - plyrPlayer.setVolume((Player.isMuted) ? 0 : 100); - }); - scope.$on('player.seekTo', function(e, percent) { - plyrPlayer.seek((percent/100) * plyrPlayer.embed.getDuration()); - }); - scope.$watch('item', function() { - try { - plyrPlayer.source({ - type: 'video', - sources: [{ - src: getYoutubeId(scope.item.url), - type: 'youtube' - }] - }); - // returns an error from a missing button. - } catch (e) {} - }); - var progressionTracker; - function getYoutubeId(url) { - var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/; - var match = url.match(regExp); - return (match && match[7].length === 11)? match[7] : false; - } - function trackProgression(media) { - $interval.cancel(progressionTracker); - progressionTracker = $interval(function() { - Player.setCurrentPosition((media.getCurrentTime() / media.getDuration()) * 100); - }, 250); - } - var playerEvents = { - error: function(event) { - $interval.cancel(progressionTracker); - Player.setCurrentPosition(0); - Player.nextItem(); - }, - playing: function(event) { - Player.setStatus('playing'); - plyrPlayer.setVolume((Player.isMuted) ? 0 : 100); - trackProgression(plyrPlayer.embed); - }, - pause: function(event) { - Player.setStatus('pause'); - }, - ended: function(event) { - $interval.cancel(progressionTracker); - Player.setCurrentPosition(0); - Player.nextItem(); - } - }; - angular.forEach(playerEvents, function(eventHandler, eventName) { - element.get(0).addEventListener(eventName, eventHandler); - }); - scope.$on('$destroy', function() { - $interval.cancel(progressionTracker); - angular.forEach(playerEvents, function(eventHandler, eventName) { - element.get(0).removeEventListener(eventName, eventHandler); - }); - plyrPlayer.destroy(); - }); - }, - template: [ - '
', - '
' - ].join('') - }; - } - angular.module('loopr.player.youtube', []).directive('youtube', YoutubeDirective); - -})(); diff --git a/app/l8pr/static/reducers/auth.js b/app/l8pr/static/reducers/auth.js new file mode 100644 index 0000000..237381f --- /dev/null +++ b/app/l8pr/static/reducers/auth.js @@ -0,0 +1,51 @@ +import { createReducer } from '../utils'; +import { + AUTH_LOGIN_USER_REQUEST, + AUTH_LOGIN_USER_SUCCESS, + AUTH_LOGIN_USER_FAILURE, + AUTH_LOGOUT_USER +} from '../constants'; + + +const initialState = { + token: null, + userName: null, + isAuthenticated: false, + isAuthenticating: false, + statusText: null +}; + +export default createReducer(initialState, { + [AUTH_LOGIN_USER_REQUEST]: (state, payload) => { + return Object.assign({}, state, { + isAuthenticating: true, + statusText: null + }); + }, + [AUTH_LOGIN_USER_SUCCESS]: (state, payload) => { + return Object.assign({}, state, { + isAuthenticating: false, + isAuthenticated: true, + token: payload.token, + userName: payload.user.email, + statusText: 'You have been successfully logged in.' + }); + }, + [AUTH_LOGIN_USER_FAILURE]: (state, payload) => { + return Object.assign({}, state, { + isAuthenticating: false, + isAuthenticated: false, + token: null, + userName: null, + statusText: `Authentication Error: ${payload.status} - ${payload.statusText}` + }); + }, + [AUTH_LOGOUT_USER]: (state, payload) => { + return Object.assign({}, state, { + isAuthenticated: false, + token: null, + userName: null, + statusText: 'You have been successfully logged out.' + }); + } +}); diff --git a/app/l8pr/static/reducers/browser.js b/app/l8pr/static/reducers/browser.js new file mode 100644 index 0000000..82deee4 --- /dev/null +++ b/app/l8pr/static/reducers/browser.js @@ -0,0 +1,21 @@ +import { RECEIVE_LOOP, SET_STRIP_STATE } from '../constants' + +export default function (state = { + loop: [], + stripOpened: false, +}, action = null) { + switch (action.type) { + case SET_STRIP_STATE: + return { + ...state, + stripOpened: action.payload === true, + } + case RECEIVE_LOOP: + return { + ...state, + loop: action.payload, + } + default: + return state + } +} diff --git a/app/l8pr/static/reducers/data.js b/app/l8pr/static/reducers/data.js new file mode 100644 index 0000000..075b05f --- /dev/null +++ b/app/l8pr/static/reducers/data.js @@ -0,0 +1,21 @@ +import { createReducer } from '../utils'; +import { DATA_RECEIVE_PROTECTED_DATA, DATA_FETCH_PROTECTED_DATA_REQUEST } from '../constants'; + +const initialState = { + data: null, + isFetching: false +}; + +export default createReducer(initialState, { + [DATA_RECEIVE_PROTECTED_DATA]: (state, payload) => { + return Object.assign({}, state, { + data: payload.data, + isFetching: false + }); + }, + [DATA_FETCH_PROTECTED_DATA_REQUEST]: (state, payload) => { + return Object.assign({}, state, { + isFetching: true + }); + } +}); diff --git a/app/l8pr/static/reducers/index.js b/app/l8pr/static/reducers/index.js new file mode 100644 index 0000000..ab71aa8 --- /dev/null +++ b/app/l8pr/static/reducers/index.js @@ -0,0 +1,14 @@ +import { combineReducers } from 'redux'; +import { routerReducer } from 'react-router-redux'; +import authReducer from './auth'; +import dataReducer from './data'; +import playerReducer from './player'; +import browserReducer from './browser'; + +export default combineReducers({ + auth: authReducer, + data: dataReducer, + routing: routerReducer, + player: playerReducer, + browser: browserReducer, +}); diff --git a/app/l8pr/static/reducers/player.js b/app/l8pr/static/reducers/player.js new file mode 100644 index 0000000..66dd12f --- /dev/null +++ b/app/l8pr/static/reducers/player.js @@ -0,0 +1,35 @@ +import * as c from '../constants' +import { get } from 'lodash' + +export default function (state = { + playlist: [], + currentTrack: undefined, + currentShow: undefined, + muted: false, + playing: false +}, action = null) { + switch (action.type) { + case c.SET_PLAYLIST: + return { + ...state, + playlist: action.payload, + } + case c.PLAY: + var showId = get(action, 'payload.show') || state.currentShow || state.playlist[0].id + var itemId = get(action, 'payload.item') || state.currentTrack || state.playlist[0].items[0].id + return { + ...state, + playing: true, + currentShow: parseInt(showId), + currentTrack: parseInt(itemId), + } + case c.PAUSE: + return { ...state, playing: false } + case c.MUTE: + return { ...state, muted: true } + case c.UNMUTE: + return { ...state, muted: false } + default: + return state + } +} diff --git a/app/l8pr/static/routes.js b/app/l8pr/static/routes.js new file mode 100644 index 0000000..e7cc7d8 --- /dev/null +++ b/app/l8pr/static/routes.js @@ -0,0 +1,15 @@ +import React from 'react' +import { Route, IndexRoute } from 'react-router' +import App from './app' +import { HomeView, LoginView, ProtectedView, NotFoundView } from './containers' +import requireAuthentication from './utils/requireAuthentication' + +export default( + + + + {/***/} + {/***/} + {/***/} + +) diff --git a/app/l8pr/static/selectors.js b/app/l8pr/static/selectors.js new file mode 100644 index 0000000..5d42c05 --- /dev/null +++ b/app/l8pr/static/selectors.js @@ -0,0 +1,50 @@ +import { createSelector } from 'reselect' +import { get } from 'lodash' + +export const currentTrack = (state) => state.player.currentTrack +export const currentShow = (state) => state.player.currentShow +export const playlist = (state) => state.player.playlist +export const getPathname = (state) => state.routing.locationBeforeTransitions.pathname +export const getCurrentShow = createSelector( + [currentShow, playlist], + (currentShowId, playlist) => { + return playlist.find((s) => s.id === currentShowId) + } +) + +export const getCurrentTrack = createSelector( + [getCurrentShow, currentTrack], + (currentShow, currentTrackId) => { + if (currentShow) { + return currentShow.items.find((i) => i.id === currentTrackId) + } + } +) + +export const getCurrentShowPositionInShow = createSelector( + [currentShow, playlist], + (currentShowId, playlist) => { + return playlist.findIndex((s) => s.id === currentShowId) + } +) + +export const getCurrentTrackPositionInShow = createSelector( + [getCurrentShow, currentTrack], + (currentShow, currentTrackId) => { + if (currentShow) { + return currentShow.items.findIndex((i) => i.id === currentTrackId) + } + } +) + +export const getLocation = createSelector( + [getPathname], + (pathname) => { + const locationRegex = /show\/([0-9]+)\/item\/([0-9]+)/g + const m = locationRegex.exec(pathname) + return { + show: get(m, '[1]'), + item: get(m, '[2]'), + } + } +) diff --git a/app/l8pr/static/store/configureStore.dev.js b/app/l8pr/static/store/configureStore.dev.js new file mode 100644 index 0000000..c97f839 --- /dev/null +++ b/app/l8pr/static/store/configureStore.dev.js @@ -0,0 +1,36 @@ +/* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ + +import thunk from 'redux-thunk'; +import { applyMiddleware, compose, createStore } from 'redux'; +import createLogger from 'redux-logger'; +import { routerMiddleware } from 'react-router-redux'; + +import rootReducer from '../reducers'; +import DevTools from '../containers/Root/DevTools'; + +export default function configureStore(initialState, history) { + const logger = createLogger(); + + // Add so dispatched route actions to the history + const reduxRouterMiddleware = routerMiddleware(history); + + const middleware = applyMiddleware(thunk, logger, reduxRouterMiddleware); + + const createStoreWithMiddleware = compose( + middleware, + DevTools.instrument() + ); + + const store = createStoreWithMiddleware(createStore)(rootReducer, initialState); + + if (module.hot) { + module.hot + .accept('../reducers', () => { + const nextRootReducer = require('../reducers/index'); // eslint-disable-line global-require + + store.replaceReducer(nextRootReducer); + }); + } + + return store; +} diff --git a/app/l8pr/static/store/configureStore.js b/app/l8pr/static/store/configureStore.js new file mode 100644 index 0000000..ba5ab00 --- /dev/null +++ b/app/l8pr/static/store/configureStore.js @@ -0,0 +1,5 @@ +if (process.env.NODE_ENV === 'production') { + module.exports = require('./configureStore.prod'); // eslint-disable-line global-require +} else { + module.exports = require('./configureStore.dev'); // eslint-disable-line global-require +} diff --git a/app/l8pr/static/store/configureStore.prod.js b/app/l8pr/static/store/configureStore.prod.js new file mode 100644 index 0000000..e010962 --- /dev/null +++ b/app/l8pr/static/store/configureStore.prod.js @@ -0,0 +1,18 @@ +import thunk from 'redux-thunk'; +import { applyMiddleware, compose, createStore } from 'redux'; +import { routerMiddleware } from 'react-router-redux'; + +import rootReducer from '../reducers'; + +export default function configureStore(initialState, history) { + // Add so dispatched route actions to the history + const reduxRouterMiddleware = routerMiddleware(history); + + const middleware = applyMiddleware(thunk, reduxRouterMiddleware); + + const createStoreWithMiddleware = compose( + middleware + ); + + return createStoreWithMiddleware(createStore)(rootReducer, initialState); +} diff --git a/app/l8pr/static/strip/add-to-shows/module.js b/app/l8pr/static/strip/add-to-shows/module.js deleted file mode 100644 index 44546f0..0000000 --- a/app/l8pr/static/strip/add-to-shows/module.js +++ /dev/null @@ -1,90 +0,0 @@ -(function() { - 'use strict'; - - ModalInstanceCtrl.$inject = ['$scope', '$uibModalInstance', 'shows', 'item', - '$q', 'Api', 'login', '$rootScope', 'ApiCache']; - function ModalInstanceCtrl($scope, $uibModalInstance, shows, item, - $q, Api, login, $rootScope, ApiCache) { - var vm = this; - angular.extend(vm, { - shows: shows, - item: item, - newShow: {}, - createShow: function() { - vm.loading = true; - $q.all([ - Api.FindOrCreateItem({url: vm.item.url}), - login.login() - ]).then(function(values) { - vm.newShow.items = [values[0]]; - vm.newShow.user = values[1].id; - Api.Shows.post(vm.newShow).then(function(show) { - $uibModalInstance.close(show); - ApiCache.isDirty = true; - $rootScope.$broadcast('l8pr.updatedLoop'); - vm.loading = false; - }); - }, function onError() { - vm.loading = false; - }); - }, - addToShow: function(show) { - if (vm.loading) {return;} - vm.loading = true; - $q.when((function() { - if (vm.item.id === null) { - return Api.FindOrCreateItem({url: vm.item.url}); - } else { - if (vm.item.id.toString().indexOf('.') > -1) { - var id = vm.item.id.split('.'); - if (id[1] === 'item') { - vm.item.id = id[2]; - } - } - return vm.item; - } - })()).then(function(item) { - show.items.unshift(item); - show.save().then(function() { - $uibModalInstance.close(show); - ApiCache.isDirty = true; - $rootScope.$broadcast('l8pr.updatedShow', show); - vm.loading = false; - }, function onError() { - vm.loading = false; - }); - }); - }, - cancel: function() { - $uibModalInstance.dismiss('cancel'); - } - }); - } - - AddToShowService.$inject = ['$uibModal']; - function AddToShowService($uibModal) { - return function(item) { - var modalInstance = $uibModal.open({ - animation: true, - templateUrl: '/strip/add-to-shows/template.html', - controller: ModalInstanceCtrl, - controllerAs: 'vm', - resolve: { - item: function() { - return item; - }, - shows: ['Api', 'login', function (Api, login) { - // FIXME: if not loged ? - return Api.Shows.getList({user: login.currentUser.id, ordering: '-updated'}); - }] - } - }); - return modalInstance.result; - }; - } - - angular.module('loopr.addToShow', []) - .factory('addToShowModal', AddToShowService); - - -})(); diff --git a/app/l8pr/static/strip/add-to-shows/template.html b/app/l8pr/static/strip/add-to-shows/template.html deleted file mode 100644 index 86f30a6..0000000 --- a/app/l8pr/static/strip/add-to-shows/template.html +++ /dev/null @@ -1,44 +0,0 @@ - - - diff --git a/app/l8pr/static/strip/feed/module.js b/app/l8pr/static/strip/feed/module.js deleted file mode 100644 index 98a3034..0000000 --- a/app/l8pr/static/strip/feed/module.js +++ /dev/null @@ -1,51 +0,0 @@ -(function() { - 'use strict'; - - FeedCtrl.$inject = ['$interval', 'Player', 'Api', '$scope']; - function FeedCtrl($interval, Player, Api, $scope) { - var vm = this; - var source = JSON.parse(Player.loop.feed_json); - var timeInterval = 5000; // ms - if (!source && source.length < 1) { - return; - } - var index = 0; - var setScope = function(tweet) { - angular.extend(vm, { - tweet: tweet - }); - }; - setScope(source[index]); - var nextTweetInterval = $interval(function() { - index += 1; - if (index >= source.length) {index = 0;} - setScope(source[index]); - }, timeInterval); - var reloadFeed = $interval(function() { - if (Player.loop.id) { - return Api.Loops.one(Player.loop.id).get().then(function(loop) { - Player.loop.feed_json = loop.feed_json; - source = JSON.parse(loop.feed_json); - }); - } - }, 60000 * 3); - $scope.$on('$destroy', function() { - // Make sure that the intervals are destroyed - $interval.cancel(reloadFeed); - $interval.cancel(nextTweetInterval); - reloadFeed = null; - nextTweetInterval = null; - }); - } - - angular.module('loopr.strip') - .controller('FeedCtrl', FeedCtrl) - .directive('l8prFeed', function() { - return { - scope: {}, - templateUrl: '/strip/feed/template.html', - controller: FeedCtrl, - controllerAs: 'vm' - }; - }); -})(); diff --git a/app/l8pr/static/strip/feed/style.less b/app/l8pr/static/strip/feed/style.less deleted file mode 100644 index 0d62b92..0000000 --- a/app/l8pr/static/strip/feed/style.less +++ /dev/null @@ -1,29 +0,0 @@ -.feed { - background-color: fade(@text-color,80); - color: fade(@body-bg, 90); - font-family: @FONT2 !important; - font-size: 92%; - padding: 15px 25px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - i { - margin-right: 10px; - color: fade(@body-bg, 60); - &:hover{ - color: @color_vimeo; - } - } - a, a:hover, a:active { - color: fade(@body-bg, 75); - font-family: @FONT2 !important; - } - a:hover { - //border-bottom: 1px solid fade(@body-bg, 70); - color: fade(@body-bg, 95); - } - span { - font-weight: bold; - } -} diff --git a/app/l8pr/static/strip/feed/template.html b/app/l8pr/static/strip/feed/template.html deleted file mode 100644 index 7cf735e..0000000 --- a/app/l8pr/static/strip/feed/template.html +++ /dev/null @@ -1,4 +0,0 @@ - - - {{ vm.tweet.name }} > {{ vm.tweet.body }} - diff --git a/app/l8pr/static/strip/header/module.js b/app/l8pr/static/strip/header/module.js deleted file mode 100644 index a041577..0000000 --- a/app/l8pr/static/strip/header/module.js +++ /dev/null @@ -1,57 +0,0 @@ -(function() { -'use strict'; - -StripHeaderCtrl.$inject = ['login', '$state', '$history', '$rootScope', 'loopToExplore']; -function StripHeaderCtrl(login, $state, $history, $rootScope, loopToExplore) { - var vm = this; - angular.extend(vm, { - searchQuery: $state.params.q, - searchBarVisible: $state.current.name === 'index.open.search' && ($state.params.q === undefined || $state.params.q === ''), - loopAuthor: loopToExplore.username, - showsCount: loopToExplore.shows_list.length, - loginService: login, - openLoginView: login.openLoginView, - exit: function() { - if ($state.current.name === 'index.open.search') { - $history.back(); - } - }, - search: function(query) { - $state.go('index.open.search', {q: query}); - vm.searchBarVisible = false; - } - }); - // login to know the current user - login.login(); - // listen for key shortcut - $rootScope.$on('openSearch', function() { - vm.searchQuery = ''; - vm.searchBarVisible = true; - }); -} - -angular.module('loopr.stripHeader', []) -.controller('StripHeaderCtrl', StripHeaderCtrl) -.directive('l8prStripHeader', [function() { - return { - scope: {}, - templateUrl: '/strip/header/template.html', - bindToController: true, - controllerAs: 'vm', - controller: StripHeaderCtrl - }; -}]) -.directive('focusMe', function($timeout) { - return { - scope: { trigger: '=focusMe' }, - link: function(scope, element) { - scope.$watch('trigger', function(value) { - if(value === true) { - element[0].focus(); - } - }); - } - }; -}); - -})(); diff --git a/app/l8pr/static/strip/header/style.less b/app/l8pr/static/strip/header/style.less deleted file mode 100644 index 5c3ccff..0000000 --- a/app/l8pr/static/strip/header/style.less +++ /dev/null @@ -1,82 +0,0 @@ -.strip__header { - background-color: @LOW_OPACITY_BACKGROUND; - //overflow: hidden; - font-family: @FONT2; - height: 50px; - padding: 15px (@MARGE * 2) 0 (@MARGE * 2); - //padding: 15px 0px 0px 0px; - @media (min-width: @screen-md){ - padding: 15px (@MARGE * 3) 0 (@MARGE * 3); - } - &__title { - .logo { - display: inline-block; - padding-right: @MARGE * 1.5; - cursor:default; - margin-left: -5px; - } - .info { - .h5; - display: inline-block; - margin-top: -5px !important; - .user-name, .user-name:active { - .text-capitalize; - margin-left: 40px; - padding-right: 3px; - color: @text-color; - } - .user-name:hover{ - color: @body-bg; - background-color: @text-color; - } - .number-shows { - font-style: italic; - } - i { - position: absolute; - } - } - } - .dropdown-menu { - left: -160px; - top: -30px; - //z-index: 9999; - border: none; - box-shadow: 10px 10px 0px @SHADOW !important; - > li > a { - margin: 0; - padding: 10px 20px; - } - } - - .current-user { - display: inline-block; - background-repeat: no-repeat; - background-size: cover; - position: relative; - width: 25px; - height: 25px; - } - &__searchbar { - input[type="text"] { - font-family: @FONT2 !important; - margin: 0; - font-size: 13px; - width: 100%; - appearance: none; - box-shadow: none; - border-radius: none; - padding: 3px 8px; - border: none; - box-shadow: 0px 0px 20px @SHADOW; - transition: border 0.3s; - top: -4px; - position: relative; - } - input[type="text"]:focus, - input[type="text"].focus { - outline: none; - border: none; - } - } -} diff --git a/app/l8pr/static/strip/header/template.html b/app/l8pr/static/strip/header/template.html deleted file mode 100644 index 43b3eff..0000000 --- a/app/l8pr/static/strip/header/template.html +++ /dev/null @@ -1,38 +0,0 @@ -
-
- - -
- search - search -
-
- account_circle - -
-
-
- account_circle -
-
-
-
diff --git a/app/l8pr/static/strip/help/module.js b/app/l8pr/static/strip/help/module.js deleted file mode 100644 index 50b903f..0000000 --- a/app/l8pr/static/strip/help/module.js +++ /dev/null @@ -1,34 +0,0 @@ -(function() { - 'use strict'; - angular.module('loopr.strip') - .factory('help', ['$uibModal', '$stateParams', function($uibModal, $stateParams) { - return { - open: function() { - return $uibModal.open({ - templateUrl: '/strip/help/template.html', - controllerAs: 'vm', - controller: ['Api', '$uibModalInstance', function(Api, $uibModalInstance) { - var vm = this; - // angular.extend(vm, { - // cancel: function() { - // $uibModalInstance.dismiss('cancel'); - // }, - // confirmReset: function() { - // vm.errors = undefined; - // Api.Register.one('password').one('reset').one('confirm').customPOST({ - // uid: $stateParams.uid, - // token: $stateParams.token, - // new_password: vm.password - // }).then(function onSuccess(d) { - // $uibModalInstance.close(); - // }, function onError(e) { - // vm.errors = e.data; - // }); - // } - // }); - }] - }); - } - }; - }]); -})(); diff --git a/app/l8pr/static/strip/help/style.less b/app/l8pr/static/strip/help/style.less deleted file mode 100644 index c891343..0000000 --- a/app/l8pr/static/strip/help/style.less +++ /dev/null @@ -1,112 +0,0 @@ -.help { - h5 { - font-family: @FONT1; - padding: 40px 0px 20px 0px; - font-weight: bold; - text-transform: uppercase; - } - p { - margin-right: 20%; - } - &__loopr-name { - color: @text-color; - font-family: @FONT1; - font-size: @font-size-h1; - //margin-left: 10px; - } - &__loopr-logo { - width: 100px; - height: 100px; - position: absolute; - right: 0px; - //text-align: right; - //margin-right: 50px; - //font-size: 200%; - i { - font-size: 48px !important; - } - } - &__loopr-punchline { - color: @brand-primary; - font-family: @FONT2; - font-size: @font-size-h5; - margin-top: 5px; - margin-bottom: 20px; - //font-style: italic; - //margin-left: 10px; - - } - &__how-to { - - ul { - //font-size: @font-size-h4; - list-style: none; - margin: 0; - padding: 0; - line-height: 2em; - } - li:hover { - //font-weight: bolder; - } - } - - &__links { - margin-top: 40px; - - a { - margin-right: 10px; - } - a:hover { - color: @brand-primary; - } - } - &__keyboard { - display: inline-block; - - &__key { - display: inline-block; - background-color: #ececec; //@text-color; - color: @text-color; - min-width: 40px; - height: 40px; - text-align: center; - border-radius: 3px; - padding-top: 10px; - font-family: @FONT1; - margin-bottom: 10px; - font-weight: normal; - padding-left: 10px; - padding-right: 10px; - } - &__key-caption { - margin-right: 20px; - margin-left: 10px; - display: inline-block; - } - } - &__plugin { - a { - font-weight: bold; - } - a:hover { - font-weight: bolder; - color: @brand-primary; - } - p { - margin-bottom: 10px; - } - } - &__footer { - .text-left; - //color: #666; - margin: 0px 15px 0px 0px; - line-height: 2em; - a { - //color: #666; - } - a:hover { - font-weight: bolder; - color: @brand-primary; - } - } -} diff --git a/app/l8pr/static/strip/help/template.html b/app/l8pr/static/strip/help/template.html deleted file mode 100644 index 8c189d0..0000000 --- a/app/l8pr/static/strip/help/template.html +++ /dev/null @@ -1,84 +0,0 @@ - - - diff --git a/app/l8pr/static/strip/loop/module.js b/app/l8pr/static/strip/loop/module.js deleted file mode 100644 index 3ae8364..0000000 --- a/app/l8pr/static/strip/loop/module.js +++ /dev/null @@ -1,135 +0,0 @@ -(function() { -'use strict'; - -LoopExplorerCtrl.$inject = ['Player', '$scope', 'strip', 'loopToExplore', -'latestItemsShow', '$state']; -function LoopExplorerCtrl(Player, scope, stripService, loopToExplore, -latestItemsShow, $state) { - var vm = this; - function reorderShows() { - var shows; - var indexOfCurrentShow = _.findIndex(loopToExplore.shows_list, function(s) {return s === Player.currentShow;}); - if (indexOfCurrentShow > -1) { - var reordered = loopToExplore.shows_list.slice(indexOfCurrentShow, loopToExplore.shows_list.length); - shows = reordered.concat(loopToExplore.shows_list.slice(0, indexOfCurrentShow)); - } else { - shows = loopToExplore.shows_list; - } - // load latest item and add it to a new show - if (!_.any(shows, function isALatestShow(s) { - return s.show_type === 'last_item'; - })) { - shows.unshift(latestItemsShow); - } - vm.shows = shows; - } - angular.extend(vm, { - Player: Player, - play: function(show) { - if (loopToExplore.id !== Player.loop.id) { - Player.playLoop(loopToExplore, show.id); - } else { - Player.playShow(show); - } - }, - stripService: stripService - }); - reorderShows(); - scope.$on('l8pr.updatedLoop', function(e, updatedLoop) { - if (!updatedLoop || updatedLoop.id === loopToExplore.id) { - $state.reload('index.open'); - } - }); -} - -angular.module('loopr.strip') -.controller('LoopExplorerCtrl', LoopExplorerCtrl) -.directive('horizontalScroll', [function() { - return { - link: function(scope, element) { - element.mousewheel(function(event, delta) { - if (event.deltaY !== 0 && Math.abs(event.deltaY) === 1) { - this.scrollLeft -= (event.deltaY * event.deltaFactor); - event.preventDefault(); - } - }); - } - }; -}]) -.directive('swapOnHover', ['$timeout', function($timeout) { - return { - template: [ - '
', - '
' - ].join(''), - scope: { - swapOnHover: '=' - }, - link: function(scope, element, attr) { - $timeout(function() { - scope.getBg = function(index) { - if(scope.swapOnHover && scope.swapOnHover.length > 0) { - return scope.swapOnHover[index % scope.swapOnHover.length].thumbnail; - } - }; - var exit; - var enter = false; - scope.index = 0; - function loadNew() { - $timeout.cancel(exit); - scope.index++; - exit = $timeout(loadNew, 2000); - } - var debounced = _.debounce(loadNew, 500, {leading: true, trailing: false}); - element.on('mouseenter', function() { - if (!enter) { - debounced(); - } - enter = true; - }); - element.on('mouseleave', function() { - enter = false; - $timeout.cancel(exit); - }); - }); - } - }; -}]) -.directive('infinitScroll', ['$timeout', function($timeout) { - return { - transclude: true, - template: ['
', - '
', - '
', - '
'].join(''), - link: function(scope, element) { - $timeout(function() { - scope.infinitScrollEnable = element.find('.infinit-scroll__item').width() > element.width(); - }); - scope.$on('$destroy', function() { - element.unbind('scroll'); - }); - scope.$watch('infinitScrollEnable', function(infinitScrollEnable) { - $timeout(function() { - if (scope.infinitScrollEnable) { - var items = element.find('.infinit-scroll__item'); - element.scrollLeft(items[1].offsetLeft); - element.scroll(function(a,b) { - if (element.scrollLeft() >= items.width() * 2 - element.width()) { - element.scrollLeft(items[1].offsetLeft - element.width()); - } - if (element.scrollLeft() === 0) { - element.scrollLeft(items.width()); - } - }); - } else { - element.unbind('scroll'); - } - }); - }); - } - }; -}]); - -})(); diff --git a/app/l8pr/static/strip/loop/style.less b/app/l8pr/static/strip/loop/style.less deleted file mode 100644 index 17e0fbc..0000000 --- a/app/l8pr/static/strip/loop/style.less +++ /dev/null @@ -1,131 +0,0 @@ -.Loop { - background-color: @LOW_OPACITY_BACKGROUND; - .infinit-scroll__item { - display: inline-block; - } - &__shows { - white-space: nowrap; - overflow-y: auto; - } - &__show { - position: relative; - width: @LOOP_SHOW_WIDTH; - height: @LOOP_SHOW_HEIGHT; - margin-right: 0px; - display: inline-block; - overflow: hidden; - background-color: fade(@gray-dark, 90); - &__icon { - position: absolute; - right: 10px; - top: 10px; - z-index: 2; - padding: 10px; - i {font-size: 1.8em !important;} - } - &__body { - font-family: @FONT2; - background-color: @body-bg; - z-index: 1; - position: relative; - white-space: normal; - padding: 20px; - padding-left: 25px; - box-shadow: inset 10px -10px 30px fade(@text-color,5); - .title { - .h5; - font-size: 110%; - font-family: @FONT1; - overflow: hidden; - margin-bottom: 10px; - } - .title:hover{ - font-weight: bold; - } - .details { - .h6; - } - } - &__background { - z-index: 0; - position: absolute; - top: 40px; - right: 0; - left: 0; - bottom: 0; - &:hover { - &:after { - position: absolute; - top: 0px; - right: 0; - left: 0px; - bottom: 0; - content: 'play_arrow'; - font-family: 'Material Icons'; - -webkit-font-feature-settings: 'liga'; - background-color: fade(@brand-primary, 15); - background-blend-mode: multiply !important; - color: @body-bg; - font-size: 2.5em; - padding-top: 90px; - text-align: center; - text-shadow: 0px 0px 15px @text-color; - } - } - .swap-animation { - background-size: cover; - background-position: top center; - height: (@LOOP_SHOW_HEIGHT + 30px); - position: absolute; - top: 0; - right: 0; - left: 0; - bottom: 0; - &.ng-enter, &.ng-leave { - transition: 1s ease-in-out all; - transition-delay: 0.15s; - } - &.ng-enter { - top:(@LOOP_SHOW_HEIGHT + 30px); - } - &.ng-enter-active { - top:0px; - } - &.ng-leave { - top:0px; - } - &.ng-leave-active { - top:(@LOOP_SHOW_HEIGHT + 30px) * -1; - } - } - } - &--current-show { - width: 420px !important; //@CURRENT_SHOW_WIDTH - background-color: fade(@brand-primary, 80); - background-blend-mode: multiply; - box-shadow: none; - .Loop__show__body { - box-shadow: inset 10px -10px 30px fade(@text-color,5); - padding-left: 30px; - background-color: transparent; - color: @body-bg ; - .title { - .h2; - font-family: @FONT1; - font-weight: bold; - height: auto; - color: @body-bg; - } - } - .Loop__show__background { - height: auto; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - opacity: .3; - } - } - } -} diff --git a/app/l8pr/static/strip/loop/template.html b/app/l8pr/static/strip/loop/template.html deleted file mode 100644 index f6b46e2..0000000 --- a/app/l8pr/static/strip/loop/template.html +++ /dev/null @@ -1,32 +0,0 @@ -
-
-
-
- new_releases -
-
- move_to_inbox -
-
- -
- {{ ::show.title }} -
-
-
- -
-
-
-
-
-
-
diff --git a/app/l8pr/static/strip/module.js b/app/l8pr/static/strip/module.js deleted file mode 100644 index 6e4b1cb..0000000 --- a/app/l8pr/static/strip/module.js +++ /dev/null @@ -1,130 +0,0 @@ - - -(function() { - 'use strict'; - - angular.module('loopr.strip', ['ngSanitize', 'ngAnimate', 'FBAngular', 'loopr.addToShow', 'loopr.stripHeader', 'loopr.showconfig']) - .factory('strip', ['$timeout', '$state', '$history', function($timeout, $state, $history) { - var hideTimeout; - var service = { - toggleController: function() { - if (!_.contains(['index'], $state.current.name)) { - $state.go($state.current.name.split('.')[0]); - } else { - $history.back('index.open.loop'); - } - }, - isAutoHideEnabled: false, - showAndHide: function() { - if (service.isAutoHideEnabled) { - $timeout.cancel(hideTimeout); - service.hideStrip(false); - hideTimeout = $timeout(function() { - service.hideStrip(true); - }, 5000, false); - } - }, - autoHideToggle: function(enable) { - $timeout.cancel(hideTimeout); - if (!angular.isDefined(enable)) { - enable = !service.isAutoHideEnabled; - } - service.isAutoHideEnabled = enable; - service.hideStrip(service.isAutoHideEnabled); - }, - stripIsHidden: false, - toggleStrip: function() { - service.stripIsHidden = !service.stripIsHidden; - }, - hideStrip: function(hidden) { - service.stripIsHidden = hidden; - } - }; - return service; - }]) - .directive('time', ['$interval', function($interval) { - return { - restrict: 'E', - link: function($scope, element) { - $interval(function() { - function checkTime(i) { - return (i < 10) ? '0' + i : i; - } - var today = new Date(), - h = checkTime(today.getHours()), - m = checkTime(today.getMinutes()); - $scope.time = h + ':' + m; - }, 2000); - }, - template: '
' - }; - }]) - .directive('bodyScrollable', ['$timeout', '$window', function($timeout, $window) { - return { - scope: { - }, - link: function($scope, element) { - var win = angular.element($window); - function setHeight() { - // remove what is above element - var maxHeight = win.height() - element.offset().top; - // remove strip bar controller space - maxHeight -= win.height() - angular.element('.strip__controls').offset().top; - element.css({ - 'max-height': maxHeight, - 'overflow-y': 'auto' - }); - } - $timeout(setHeight); - win.on('resize', setHeight); - } - }; - }]) - .directive('banner', ['$interval', function($interval) { - return { - restrict: 'E', - scope: { - 'lines': '=', - 'transition': '@', - 'duration': '@' - }, - link: function($scope, element) { - var transitions = { - 'fade': [{opacity: 0}, {opacity: 1}], - 'scroll': [{top: -70}, {top: 0}] - }; - var animation; - var body = element.find('.body'); - - function showTitle(title, fast) { - if (!angular.isDefined(fast)) { - fast = false; - } - body.stop().animate(transitions[$scope.transition][0], fast? 0 : 1000, function() { - $(this).html(title); - body.stop().animate(transitions[$scope.transition][1], 1000); - }); - } - - $scope.$watch('lines', function(new_value) { - if (!angular.isDefined(new_value)) {return;} - // show title - showTitle(new_value[0], true); - // loop over titles - if (new_value.length > 1) { - var current_title = new_value[1]; - $interval.cancel(animation); - animation = $interval(function() { - showTitle(current_title); - current_title = new_value[(new_value.indexOf(current_title) + 1) % new_value.length]; - }, parseInt($scope.duration, 10) * 1000); - } - }); - $scope.$on('$destroy', function() { - $interval.cancel(animation); - }); - }, - template: '
' - }; - }]); -})(); diff --git a/app/l8pr/static/strip/reset-password/module.js b/app/l8pr/static/strip/reset-password/module.js deleted file mode 100644 index 236a4e6..0000000 --- a/app/l8pr/static/strip/reset-password/module.js +++ /dev/null @@ -1,34 +0,0 @@ -(function() { - 'use strict'; - angular.module('loopr.strip') - .factory('resetPassword', ['$uibModal', '$stateParams', function($uibModal, $stateParams) { - return { - open: function() { - return $uibModal.open({ - templateUrl: '/strip/reset-password/template.html', - controllerAs: 'vm', - controller: ['Api', '$uibModalInstance', function(Api, $uibModalInstance) { - var vm = this; - angular.extend(vm, { - cancel: function() { - $uibModalInstance.dismiss('cancel'); - }, - confirmReset: function() { - vm.errors = undefined; - Api.Register.one('password').one('reset').one('confirm').customPOST({ - uid: $stateParams.uid, - token: $stateParams.token, - new_password: vm.password - }).then(function onSuccess(d) { - $uibModalInstance.close(); - }, function onError(e) { - vm.errors = e.data; - }); - } - }); - }] - }); - } - }; - }]); -})(); diff --git a/app/l8pr/static/strip/reset-password/template.html b/app/l8pr/static/strip/reset-password/template.html deleted file mode 100644 index 957db31..0000000 --- a/app/l8pr/static/strip/reset-password/template.html +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/app/l8pr/static/strip/search-results/module.js b/app/l8pr/static/strip/search-results/module.js deleted file mode 100644 index 3d2958a..0000000 --- a/app/l8pr/static/strip/search-results/module.js +++ /dev/null @@ -1,55 +0,0 @@ -(function() { -'use strict'; - -SearchCtrl.$inject = ['query', 'results', 'Player', 'addToShowModal', 'Api', '$scope']; -function SearchCtrl(query, results, Player, addToShowModal, Api, $scope) { - var vm = this; - angular.extend(vm, { - query: query, - results: angular.copy(results), - filters: { - YouTube: true, - SoundCloud: true, - Vimeo: true - }, - getResults: function() { - var activeFiltersKeys = Object.keys(vm.filters); - var activeFilters = activeFiltersKeys.filter(function(key) { - return vm.filters[key]; - }); - return _.filter(vm.results, function(r) { - if (!r.provider_name) { - return true; - } - return _.contains(activeFilters, r.provider_name); - }); - }, - play: function(item) { - Player.playItem(item); - }, - addItemToAShow: function openModal(item) { - addToShowModal(item); - } - }); - if (query) { - // // adds loop results - // Api.Loops.getList({username: query}).then(function(results) { - // var pouet = _.map(results, function(r) { - // return { - // title: query + '\'s loop', - // type: 'loop' - // }; - // }); - // vm.results = vm.results.concat(pouet); - // }); - // adds youtube results - Api.SearchYoutube.getList({q: query}).then(function(results) { - vm.results = vm.results.concat(results); - }); - } -} - -angular.module('loopr.strip') -.controller('SearchCtrl', SearchCtrl); - -})(); diff --git a/app/l8pr/static/strip/search-results/style.less b/app/l8pr/static/strip/search-results/style.less deleted file mode 100644 index acf58d3..0000000 --- a/app/l8pr/static/strip/search-results/style.less +++ /dev/null @@ -1,18 +0,0 @@ -.Search { - .row; - overflow-y: hidden; - box-sizing: border-box; - padding: 0 0; - background-color: @LOW_OPACITY_BACKGROUND; - @media (max-width: @screen-xs){padding: 0px 0px;;} - &__cover { - .cover; - } - &__side-bar{ - .side-bar; - } - &__list { - .list; - min-height: 25vh; - } -} diff --git a/app/l8pr/static/strip/search-results/template.html b/app/l8pr/static/strip/search-results/template.html deleted file mode 100644 index cd9b3cc..0000000 --- a/app/l8pr/static/strip/search-results/template.html +++ /dev/null @@ -1,80 +0,0 @@ - diff --git a/app/l8pr/static/strip/show-config/module.js b/app/l8pr/static/strip/show-config/module.js deleted file mode 100644 index ff6add3..0000000 --- a/app/l8pr/static/strip/show-config/module.js +++ /dev/null @@ -1,46 +0,0 @@ -(function() { - 'use strict'; - - ModalInstanceCtrl.$inject = ['$uibModalInstance', 'show', 'Restangular']; - function ModalInstanceCtrl($uibModalInstance, show, Restangular) { - var vm = this; - angular.extend(vm, { - shows: show, - settings: angular.copy(show.settings), - save: function() { - var newShow = Restangular.copy(show); - angular.extend(newShow, {settings: vm.settings}); - newShow.save().then(function(savedShow) { - show.settings = savedShow.settings; - $uibModalInstance.close(show); - }); - }, - cancel: function() { - $uibModalInstance.dismiss('cancel'); - } - }); - } - - ShowConfigFactory.$inject = ['$uibModal']; - function ShowConfigFactory($uibModal) { - return function(show) { - var modalInstance = $uibModal.open({ - animation: true, - templateUrl: '/strip/show-config/template.html', - controller: ModalInstanceCtrl, - controllerAs: 'vm', - resolve: { - show: function() { - return show; - } - } - }); - return modalInstance.result; - }; - } - - angular.module('loopr.showconfig', []) - .factory('showConfig', ShowConfigFactory); - - -})(); diff --git a/app/l8pr/static/strip/show-config/template.html b/app/l8pr/static/strip/show-config/template.html deleted file mode 100644 index fb5c29c..0000000 --- a/app/l8pr/static/strip/show-config/template.html +++ /dev/null @@ -1,43 +0,0 @@ - -
- - -
diff --git a/app/l8pr/static/strip/show/module.js b/app/l8pr/static/strip/show/module.js deleted file mode 100644 index f5c6f45..0000000 --- a/app/l8pr/static/strip/show/module.js +++ /dev/null @@ -1,48 +0,0 @@ -(function() { -'use strict'; - -ShowExplorerCtrl.$inject = ['Player', '$scope', 'strip', 'show', 'showConfig', -'addToShowModal', 'Api', '$state', '$confirm', 'Restangular']; -function ShowExplorerCtrl(Player, scope, stripService, show, showConfig, -addToShowModal, Api, $state, $confirm, Restangular) { - var vm = this; - angular.extend(vm, { - stripService: stripService, - show: show, - playingNow: Player.currentShow === show, - player: Player, - showConfig: function() { - showConfig(show); - }, - removeItem: function(item) { - $confirm({text: 'Are you sure you want to delete?'}).then(function() { - var oldShow = Restangular.copy(show); - _.remove(oldShow.items, function(i) { - // FIXME: item can have no id - return item.id === i.id; - }); - oldShow.items = angular.copy(oldShow.items); - return oldShow.put().then(function(s) { - show.items = s.items; - }); - }); - }, - addItemToAShow: addToShowModal - }); - scope.$on('l8pr.updatedShow', function(e, updatedShow) { - if (show.id === updatedShow.id) { - $state.reload($state.current.name); - } - }); - scope.$watch(function() { - return Player.currentShow; - }, function(currentShow) { - vm.playingNow = currentShow === show; - }); -} - - -angular.module('loopr.strip') -.controller('ShowExplorerCtrl', ShowExplorerCtrl); - -})(); diff --git a/app/l8pr/static/strip/show/style.less b/app/l8pr/static/strip/show/style.less deleted file mode 100644 index a0a3de5..0000000 --- a/app/l8pr/static/strip/show/style.less +++ /dev/null @@ -1,21 +0,0 @@ -.Show { - overflow-y: hidden; - box-sizing: border-box; - padding: 0 ;//@MARGE; - background-color: @LOW_OPACITY_BACKGROUND; - @media (max-width: @screen-xs){padding: 0px 0px;} - &__cover { - .cover; - &__title { - font-weight: bold; - } - } - &__side-bar { - .side-bar; - } - &__list { - .list; - .list__item { - } - } -} diff --git a/app/l8pr/static/strip/show/template.html b/app/l8pr/static/strip/show/template.html deleted file mode 100644 index 4f20f7b..0000000 --- a/app/l8pr/static/strip/show/template.html +++ /dev/null @@ -1,50 +0,0 @@ -
-
-
-
-
Show by
-
{{ vm.show.items.length }} media / {{ ::vm.show.duration() | seconds }}
-
-
- - arrow_back - - settings -
-
-
-
-
- -
- -
-
- -
-
- playlist_add - clear -
-
-
-
diff --git a/app/l8pr/static/strip/style.less b/app/l8pr/static/strip/style.less deleted file mode 100644 index c35cc09..0000000 --- a/app/l8pr/static/strip/style.less +++ /dev/null @@ -1,110 +0,0 @@ -.strip { - display: block; - .icon:hover {color: @brand-primary;} - &__body { - max-height: @STRIP_MAX_HEIGHT; - } - &__feed { - background-color: fade(@body-bg, 85); - padding: 5px 0; - position: absolute; - width: 100%; - } - &__controls { - box-shadow: 0px -10px 20px @SHADOW; - position: relative; - // title + btn for toggle controller - > .row:first-child { - > div { - background-color: @body-bg; - height: @UPPER_STRIP_HEIGHT;} - } - &--extended { - > .row:first-child { - div:nth-child(2) { - background-color: fade(@body-bg, 85); - } - } - } - .banner { - transition-duration: 1s; - transition-property: height; - overflow: hidden; - padding-left: 22px !important; - padding-right: 20px !important; - .banner__playButton { - color: @brand-primary; - } - .banner__title { - i { - color: blue; - margin-top: -30px; - } - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: normal; - padding-top: 18px; - font-weight: 500; - .text-left; - - @media (min-width: @screen-md){ - margin-left: -20px; - font-size: 120%; - } - } - .closed { - padding-left: 10px; - @media (min-width: @screen-md){ - padding-left: 20px; - } - } - - .fa-youtube-play:hover { - color: @color_youtube; - } - .fa-soundcloud:hover { - color: @color_soundcloud; - } - .fa-vimeo-square:hover { - color: @color_vimeo; - } - } - .controller { - padding-right: (@MARGE * 3) !important; - - &__prevPlayNext { - padding-left: 20px !important; - } - &__help { - } - &__display { - background-color: fade(@body-bg, 0) !important; - } - &__toggle { - } - &__toggle--closed { - i { - padding-right: (@MARGE * 3); - } - } - } - i { - top: (@UPPER_STRIP_HEIGHT / 3); - position: relative; - - } - i.sm.material-icons { - } - .progress { - margin-bottom: 0px; - } - } -// // actions state button -// .list-show .active {color: @GREEN;} -// .fullscreen.active {opacity: 1;} -// .fullscreen:not(.active) {opacity: .6;} -// .share.active {color: @GREEN;} -// .auto-hide.active {opacity: 1;} -// .auto-hide:not(.active) {opacity: .6;} -} diff --git a/app/l8pr/static/styles/config/_colors.scss b/app/l8pr/static/styles/config/_colors.scss new file mode 100644 index 0000000..ef583b5 --- /dev/null +++ b/app/l8pr/static/styles/config/_colors.scss @@ -0,0 +1,16 @@ +$color-white: #ffffff; + +$color-silver-dark: #828282; +$color-silver: #a5a5a5; +$color-silver-light: #f8f8f8; + +$color-red-dark: #d45161; +$color-red: #f27d72; +$color-red-light: #ffafaf; +$color-red-light-2: #ffc5c5; + +$color-green-default: #16967a; +$color-green: #89d78f; +$color-green-light: #b3ffd8; + +$color-blue: #35A1FB; \ No newline at end of file diff --git a/app/l8pr/static/styles/config/_fonts.scss b/app/l8pr/static/styles/config/_fonts.scss new file mode 100644 index 0000000..e69de29 diff --git a/app/l8pr/static/styles/config/_reset.scss b/app/l8pr/static/styles/config/_reset.scss new file mode 100644 index 0000000..b9e7968 --- /dev/null +++ b/app/l8pr/static/styles/config/_reset.scss @@ -0,0 +1,51 @@ +/* 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: none; + 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; +} +a{ + text-decoration: none; +} diff --git a/app/l8pr/static/styles/config/_typography.scss b/app/l8pr/static/styles/config/_typography.scss new file mode 100644 index 0000000..4ef14f5 --- /dev/null +++ b/app/l8pr/static/styles/config/_typography.scss @@ -0,0 +1,61 @@ +h1 { + font-family: $font-family-regular; + font-size: 3rem; + color: $color-silver-dark; + padding-bottom: .6rem; +} + +h2 { + font-family: $font-family-regular; + font-size: 2.5rem; + color: $color-silver-dark; +} + +h3 { + font-family: $font-family-regular; + font-size: 2rem; + color: $color-silver-dark; +} + +h4 { + font-family: $font-family-regular; + font-size: 1.5rem; + color: $color-silver-dark; +} + +h5 { + font-family: $font-family-regular; + font-size: 1rem; + color: $color-silver-dark; +} + +p { + font-family: $font-family-regular; + font-size: 1rem; + line-height: 2rem; + color: $color-silver-dark; +} + +a { + font-family: $font-family-regular; + font-size: 1rem; + text-decoration: underline; + + &:hover { + color: $color-silver-dark; + cursor: pointer; + } +} + +b, strong { + font-weight: bold; +} + +code { + padding: 2px 4px; +} + +label { + font-family: $font-family-regular; + color: $color-silver-dark; +} \ No newline at end of file diff --git a/app/l8pr/static/styles/config/_variables.scss b/app/l8pr/static/styles/config/_variables.scss new file mode 100644 index 0000000..5d562ad --- /dev/null +++ b/app/l8pr/static/styles/config/_variables.scss @@ -0,0 +1,3 @@ +@import "colors"; + +$font-family-regular: Arial; diff --git a/app/l8pr/static/styles/font-awesome.config.js b/app/l8pr/static/styles/font-awesome.config.js new file mode 100644 index 0000000..1ee6135 --- /dev/null +++ b/app/l8pr/static/styles/font-awesome.config.js @@ -0,0 +1,18 @@ +/** + * Configuration file for font-awesome-webpack + * + * In order to keep the bundle size low in production, + * disable components you don't use. + * + */ + +module.exports = { + styles: { + mixins: true, + core: true, + icons: true, + larger: true, + path: true, + animated: true + } +}; diff --git a/app/l8pr/static/styles/font-awesome.config.less b/app/l8pr/static/styles/font-awesome.config.less new file mode 100644 index 0000000..99d3b3b --- /dev/null +++ b/app/l8pr/static/styles/font-awesome.config.less @@ -0,0 +1,7 @@ +/** + * Configuration file for font-awesome-webpack + * + */ + +// Example: +// @fa-border-color: #ddd; diff --git a/app/l8pr/static/styles/font-awesome.config.prod.js b/app/l8pr/static/styles/font-awesome.config.prod.js new file mode 100644 index 0000000..0cda4f8 --- /dev/null +++ b/app/l8pr/static/styles/font-awesome.config.prod.js @@ -0,0 +1,9 @@ +const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const fontAwesomeConfig = require('./font-awesome.config'); + +fontAwesomeConfig.styleLoader = ExtractTextPlugin.extract({ + fallbackLoader: 'style-loader', + loader: 'css-loader!less-loader' +}); + +module.exports = fontAwesomeConfig; diff --git a/app/l8pr/static/styles/main.scss b/app/l8pr/static/styles/main.scss new file mode 100644 index 0000000..28649cf --- /dev/null +++ b/app/l8pr/static/styles/main.scss @@ -0,0 +1,48 @@ +@import "config/reset"; +@import "config/fonts"; +@import "config/typography"; + +@import "theme/base"; +@import "theme/login"; +@import "theme/navbar"; +@import "theme/footer"; + +@import "utils/margins"; + + +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(~material-design-icons/iconfont/MaterialIcons-Regular.eot); /* For IE6-8 */ + src: local('Material Icons'), + local('MaterialIcons-Regular'), + url(~material-design-icons/iconfont/MaterialIcons-Regular.woff2) format('woff2'), + url(~material-design-icons/iconfont/MaterialIcons-Regular.woff) format('woff'), + url(~material-design-icons/iconfont/MaterialIcons-Regular.ttf) format('truetype'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; /* Preferred icon size */ + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + + /* Support for all WebKit browsers. */ + -webkit-font-smoothing: antialiased; + /* Support for Safari and Chrome. */ + text-rendering: optimizeLegibility; + + /* Support for Firefox. */ + -moz-osx-font-smoothing: grayscale; + + /* Support for IE. */ + font-feature-settings: 'liga'; +} diff --git a/app/l8pr/static/styles/theme/_base.scss b/app/l8pr/static/styles/theme/_base.scss new file mode 100644 index 0000000..14171c4 --- /dev/null +++ b/app/l8pr/static/styles/theme/_base.scss @@ -0,0 +1,6 @@ +html { + width: 100%; + height: 100%; + font-size: 100%; + font-family: $font-family-regular; +} \ No newline at end of file diff --git a/app/l8pr/static/styles/theme/_footer.scss b/app/l8pr/static/styles/theme/_footer.scss new file mode 100644 index 0000000..e69de29 diff --git a/app/l8pr/static/styles/theme/_login.scss b/app/l8pr/static/styles/theme/_login.scss new file mode 100644 index 0000000..e7ff409 --- /dev/null +++ b/app/l8pr/static/styles/theme/_login.scss @@ -0,0 +1,7 @@ +.login { + .login-container { + max-width: 600px; + margin: 0 auto; + padding: 20px; + } +} \ No newline at end of file diff --git a/app/l8pr/static/styles/theme/_navbar.scss b/app/l8pr/static/styles/theme/_navbar.scss new file mode 100644 index 0000000..f99ad73 --- /dev/null +++ b/app/l8pr/static/styles/theme/_navbar.scss @@ -0,0 +1,7 @@ +.navbar { + border-radius: 0px; + + a { + text-decoration: none; + } +} \ No newline at end of file diff --git a/app/l8pr/static/styles/utils/_margins.scss b/app/l8pr/static/styles/utils/_margins.scss new file mode 100644 index 0000000..be5c4d0 --- /dev/null +++ b/app/l8pr/static/styles/utils/_margins.scss @@ -0,0 +1,37 @@ +.margin-none { + margin: 0; +} + +// Margins top +.margin-top-none { + margin-top: 0; +} + +.margin-top-small { + margin-top: .6rem; +} + +.margin-top-medium { + margin-top: 1.875rem; +} + +.margin-top-large { + margin-top: 3.7rem; +} + +// Margins bottom +.margin-bottom-none { + margin-bottom: 0; +} + +.margin-bottom-small { + margin-bottom: .6rem; +} + +.margin-bottom-medium { + margin-bottom: 1.875rem; +} + +.margin-bottom-large { + margin-bottom: 3.7rem; +} diff --git a/app/l8pr/static/utils/config.js b/app/l8pr/static/utils/config.js new file mode 100644 index 0000000..ae5d643 --- /dev/null +++ b/app/l8pr/static/utils/config.js @@ -0,0 +1,5 @@ +export const SERVER_URL = 'http://localhost:8000'; + +// config should use named export as there can be different exports, +// just need to export default also because of eslint rules +export { SERVER_URL as default }; diff --git a/app/l8pr/static/utils/index.js b/app/l8pr/static/utils/index.js new file mode 100644 index 0000000..2a763d7 --- /dev/null +++ b/app/l8pr/static/utils/index.js @@ -0,0 +1,20 @@ +export function createReducer(initialState, reducerMap) { + return (state = initialState, action) => { + const reducer = reducerMap[action.type]; + return reducer ? reducer(state, action.payload) : state; + }; +} + +export function checkHttpStatus(response) { + if (response.status >= 200 && response.status < 300) { + return response; + } + + const error = new Error(response.statusText); + error.response = response; + throw error; +} + +export function parseJSON(response) { + return response.json(); +} diff --git a/app/l8pr/static/utils/requireAuthentication.js b/app/l8pr/static/utils/requireAuthentication.js new file mode 100644 index 0000000..1f0c926 --- /dev/null +++ b/app/l8pr/static/utils/requireAuthentication.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { push } from 'react-router-redux'; + +export default function requireAuthentication(Component) { + class AuthenticatedComponent extends React.Component { + + static propTypes = { + isAuthenticated: React.PropTypes.bool.isRequired, + location: React.PropTypes.shape({ + pathname: React.PropTypes.string.isRequired + }).isRequired, + dispatch: React.PropTypes.func.isRequired + }; + + componentWillMount() { + this.checkAuth(); + } + + componentWillReceiveProps(nextProps) { + this.checkAuth(); + } + + checkAuth() { + if (!this.props.isAuthenticated) { + const redirectAfterLogin = this.props.location.pathname; + this.props.dispatch(push(`/login?next=${redirectAfterLogin}`)); + } + } + + render() { + return ( +
+ {this.props.isAuthenticated === true + ? + : null + } +
+ ); + } + } + + const mapStateToProps = (state) => { + return { + isAuthenticated: state.auth.isAuthenticated, + token: state.auth.token + }; + }; + + return connect(mapStateToProps)(AuthenticatedComponent); +} diff --git a/app/l8pr/static/variables.less b/app/l8pr/static/variables.less deleted file mode 100644 index 0e3bc54..0000000 --- a/app/l8pr/static/variables.less +++ /dev/null @@ -1,910 +0,0 @@ -// -// Variables L8pr - -// General -@PLAYING_NOW_COLOR: @brand-primary; -@GREEN: @brand-primary; // remove later, replace by @brand-primary -@LOW_OPACITY_BACKGROUND: fade(@body-bg, 85); - -@MARGE_OUTER:50px; -@MARGE_OUTER--mobile: 0px; - -@MARGE: @line-height-computed; - -@STRIP_MAX_HEIGHT: 55vh; - -@SHADOW: fade(@text-color, 20); -@color_youtube: red; -@color_soundcloud: #f50; -@color_vimeo: #43BBFF; - -@color-list-odd: darken(@body-bg, 2); //rgba(240,240,240,1); -@color-list-even: darken(@body-bg, 2); - - -// Banner -@UPPER_STRIP_HEIGHT: 60px; - -// Loop -@LOOP_SHOW_WIDTH: 260px; -@LOOP_SHOW_HEIGHT: 230px; -@CELL_HEIGHT: 50px; - -// Shows -@SHOW_COVER_COLOR: fade(@gray-darker, 70); - -// Fonts -@FONT1: 'Montserrat', 'Trebuchet Ms', sans-serif; -@FONT2: 'Roboto Mono', 'Andale Mono', monospace; - - - -// -// Variables -// -------------------------------------------------- - - -//== Colors -// -//## Gray and brand colors for use across Bootstrap. - -@gray-base: #000; -@gray-darker: lighten(@gray-base, 13.5%); // #222 -@gray-dark: lighten(@gray-base, 20%); // #333 -@gray: lighten(@gray-base, 33.5%); // #555 -@gray-light: lighten(@gray-base, 46.7%); // #777 -@gray-lighter: lighten(@gray-base, 93.5%); // #eee - -@brand-primary: purple; // darken(#428bca, 6.5%); // #337ab7 -@brand-success: #5cb85c; -@brand-info: fade(@brand-primary, 50); //#5bc0de; -@brand-warning: fade(@gray, 50); //#f0ad4e; -@brand-danger: #d9534f; - - -//== Scaffolding -// -//## Settings for some of the most global styles. - -//** Background color for ``. -@body-bg: #fff; -//** Global text color on ``. -@text-color: @gray-darker; - -//** Global textual link color. -@link-color: black; -//** Link hover color set via `darken()` function. -@link-hover-color: @brand-primary; -//** Link hover decoration. -@link-hover-decoration: none; - - -//== Typography -// -//## Font, line-height, and color for body text, headings, and more. - -@font-family-sans-serif: @FONT1, "Helvetica Neue", Helvetica, Arial, sans-serif; -@font-family-serif: @FONT2, Georgia, "Times New Roman", Times, serif; -//** Default monospace fonts for ``, ``, and `
`.
-@font-family-monospace:   @FONT2, Menlo, Monaco, Consolas, "Courier New", monospace;
-@font-family-base:        @font-family-sans-serif;
-
-@font-size-base:          14px;
-@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
-@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
-
-@font-size-h1:            floor((@font-size-base * 2.6)); // ~36px
-@font-size-h2:            floor((@font-size-base * 2.15)); // ~30px
-@font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px
-@font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px
-@font-size-h5:            @font-size-base;
-@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
-
-//** Unit-less `line-height` for use in components like buttons.
-@line-height-base:        1.428571429; // 20/14
-//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
-@line-height-computed:    10px; //floor((@font-size-base * @line-height-base));
-
-//** By default, this inherits from the ``.
-@headings-font-family:    inherit;
-@headings-font-weight:    500;
-@headings-line-height:    1.3;
-@headings-color:          inherit;
-
-
-//== Iconography
-//
-//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
-
-//** Load fonts from this directory.
-@icon-font-path:          "../fonts/";
-//** File name for all font files.
-@icon-font-name:          "glyphicons-halflings-regular";
-//** Element ID within SVG icon file.
-@icon-font-svg-id:        "glyphicons_halflingsregular";
-
-
-//== Components
-//
-//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-
-@padding-base-vertical:     6px;
-@padding-base-horizontal:   12px;
-
-@padding-large-vertical:    10px;
-@padding-large-horizontal:  16px;
-
-@padding-small-vertical:    5px;
-@padding-small-horizontal:  10px;
-
-@padding-xs-vertical:       1px;
-@padding-xs-horizontal:     5px;
-
-@line-height-large:         1.3333333; // extra decimals for Win 8.1 Chrome
-@line-height-small:         1.5;
-
-@border-radius-base:        0px;
-@border-radius-large:       0px;
-@border-radius-small:       0px;
-
-//** Global color for active items (e.g., navs or dropdowns).
-@component-active-color:    #fff;
-//** Global background color for active items (e.g., navs or dropdowns).
-@component-active-bg:       @brand-primary;
-
-//** Width of the `border` for generating carets that indicator dropdowns.
-@caret-width-base:          4px;
-//** Carets increase slightly in size for larger components.
-@caret-width-large:         5px;
-
-
-//== Tables
-//
-//## Customizes the `.table` component with basic values, each used across all table variations.
-
-//** Padding for ``s and ``s.
-@table-cell-padding:            8px;
-//** Padding for cells in `.table-condensed`.
-@table-condensed-cell-padding:  5px;
-
-//** Default background color used for all tables.
-@table-bg:                      transparent;
-//** Background color used for `.table-striped`.
-@table-bg-accent:               #f9f9f9;
-//** Background color used for `.table-hover`.
-@table-bg-hover:                #f5f5f5;
-@table-bg-active:               @table-bg-hover;
-
-//** Border color for table and cell borders.
-@table-border-color:            #ddd;
-
-
-//== Buttons
-//
-//## For each of Bootstrap's buttons, define text, background and border color.
-
-@btn-font-weight:                normal;
-
-@btn-default-color:              #333;
-@btn-default-bg:                 #fff;
-@btn-default-border:             #ccc;
-
-@btn-primary-color:              #fff;
-@btn-primary-bg:                 @brand-primary;
-@btn-primary-border:             darken(@btn-primary-bg, 5%);
-
-@btn-success-color:              #fff;
-@btn-success-bg:                 @brand-success;
-@btn-success-border:             darken(@btn-success-bg, 5%);
-
-@btn-info-color:                 #fff;
-@btn-info-bg:                    @brand-info;
-@btn-info-border:                darken(@btn-info-bg, 5%);
-
-@btn-warning-color:              #fff;
-@btn-warning-bg:                 @brand-warning;
-@btn-warning-border:             darken(@btn-warning-bg, 5%);
-
-@btn-danger-color:               #fff;
-@btn-danger-bg:                  @brand-danger;
-@btn-danger-border:              darken(@btn-danger-bg, 5%);
-
-@btn-link-disabled-color:        @gray-light;
-
-// Allows for customizing button radius independently from global border radius
-@btn-border-radius-base:         @border-radius-base;
-@btn-border-radius-large:        @border-radius-large;
-@btn-border-radius-small:        @border-radius-small;
-
-
-//== Forms
-//
-//##
-
-//** `` background color
-@input-bg:                       #fff;
-//** `` background color
-@input-bg-disabled:              @gray-lighter;
-
-//** Text color for ``s
-@input-color:                    @gray;
-//** `` border color
-@input-border:                   #ccc;
-
-// TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
-//** Default `.form-control` border radius
-// This has no effect on ``s in CSS.
-@input-border-radius:            @border-radius-base;
-//** Large `.form-control` border radius
-@input-border-radius-large:      @border-radius-large;
-//** Small `.form-control` border radius
-@input-border-radius-small:      @border-radius-small;
-
-//** Border color for inputs on focus
-@input-border-focus:             #66afe9;
-
-//** Placeholder text color
-@input-color-placeholder:        #999;
-
-//** Default `.form-control` height
-@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
-//** Large `.form-control` height
-@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
-//** Small `.form-control` height
-@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
-
-//** `.form-group` margin
-@form-group-margin-bottom:       15px;
-
-@legend-color:                   @gray-dark;
-@legend-border-color:            #e5e5e5;
-
-//** Background color for textual input addons
-@input-group-addon-bg:           @gray-lighter;
-//** Border color for textual input addons
-@input-group-addon-border-color: @input-border;
-
-//** Disabled cursor for form controls and buttons.
-@cursor-disabled:                not-allowed;
-
-
-//== Dropdowns
-//
-//## Dropdown menu container and contents.
-
-//** Background for the dropdown menu.
-@dropdown-bg:                    #fff;
-//** Dropdown menu `border-color`.
-@dropdown-border:                rgba(0,0,0,.15);
-//** Dropdown menu `border-color` **for IE8**.
-@dropdown-fallback-border:       #ccc;
-//** Divider color for between dropdown items.
-@dropdown-divider-bg:            #e5e5e5;
-
-//** Dropdown link text color.
-@dropdown-link-color:            @gray-dark;
-//** Hover color for dropdown links.
-@dropdown-link-hover-color:      darken(@gray-dark, 5%);
-//** Hover background for dropdown links.
-@dropdown-link-hover-bg:         #f5f5f5;
-
-//** Active dropdown menu item text color.
-@dropdown-link-active-color:     @component-active-color;
-//** Active dropdown menu item background color.
-@dropdown-link-active-bg:        @component-active-bg;
-
-//** Disabled dropdown menu item background color.
-@dropdown-link-disabled-color:   @gray-light;
-
-//** Text color for headers within dropdown menus.
-@dropdown-header-color:          @gray-light;
-
-//** Deprecated `@dropdown-caret-color` as of v3.1.0
-@dropdown-caret-color:           #000;
-
-
-//-- Z-index master list
-//
-// Warning: Avoid customizing these values. They're used for a bird's eye view
-// of components dependent on the z-axis and are designed to all work together.
-//
-// Note: These variables are not generated into the Customizer.
-
-@zindex-navbar:            1000;
-@zindex-dropdown:          1000;
-@zindex-popover:           1060;
-@zindex-tooltip:           1070;
-@zindex-navbar-fixed:      1030;
-@zindex-modal-background:  1040;
-@zindex-modal:             1050;
-
-
-//== Media queries breakpoints
-//
-//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
-
-// Extra small screen / phone
-//** Deprecated `@screen-xs` as of v3.0.1
-@screen-xs:                  480px;
-//** Deprecated `@screen-xs-min` as of v3.2.0
-@screen-xs-min:              @screen-xs;
-//** Deprecated `@screen-phone` as of v3.0.1
-@screen-phone:               @screen-xs-min;
-
-// Small screen / tablet
-//** Deprecated `@screen-sm` as of v3.0.1
-@screen-sm:                  768px;
-@screen-sm-min:              @screen-sm;
-//** Deprecated `@screen-tablet` as of v3.0.1
-@screen-tablet:              @screen-sm-min;
-
-// Medium screen / desktop
-//** Deprecated `@screen-md` as of v3.0.1
-@screen-md:                  992px;
-@screen-md-min:              @screen-md;
-//** Deprecated `@screen-desktop` as of v3.0.1
-@screen-desktop:             @screen-md-min;
-
-// Large screen / wide desktop
-//** Deprecated `@screen-lg` as of v3.0.1
-@screen-lg:                  1200px;
-@screen-lg-min:              @screen-lg;
-//** Deprecated `@screen-lg-desktop` as of v3.0.1
-@screen-lg-desktop:          @screen-lg-min;
-
-// So media queries don't overlap when required, provide a maximum
-@screen-xs-max:              (@screen-sm-min - 1);
-@screen-sm-max:              (@screen-md-min - 1);
-@screen-md-max:              (@screen-lg-min - 1);
-
-
-//== Grid system
-//
-//## Define your custom responsive grid.
-
-//** Number of columns in the grid.
-@grid-columns:              12;
-//** Padding between columns. Gets divided in half for the left and right.
-@grid-gutter-width:         30px;
-// Navbar collapse
-//** Point at which the navbar becomes uncollapsed.
-@grid-float-breakpoint:     @screen-sm-min;
-//** Point at which the navbar begins collapsing.
-@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
-
-
-//== Container sizes
-//
-//## Define the maximum width of `.container` for different screen sizes.
-
-// Small screen / tablet
-@container-tablet:             (720px + @grid-gutter-width);
-//** For `@screen-sm-min` and up.
-@container-sm:                 @container-tablet;
-
-// Medium screen / desktop
-@container-desktop:            (940px + @grid-gutter-width);
-//** For `@screen-md-min` and up.
-@container-md:                 @container-desktop;
-
-// Large screen / wide desktop
-@container-large-desktop:      (1140px + @grid-gutter-width);
-//** For `@screen-lg-min` and up.
-@container-lg:                 @container-large-desktop;
-
-
-//== Navbar
-//
-//##
-
-// Basics of a navbar
-@navbar-height:                    50px;
-@navbar-margin-bottom:             @line-height-computed;
-@navbar-border-radius:             @border-radius-base;
-@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
-@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
-@navbar-collapse-max-height:       340px;
-
-@navbar-default-color:             #777;
-@navbar-default-bg:                #f8f8f8;
-@navbar-default-border:            darken(@navbar-default-bg, 6.5%);
-
-// Navbar links
-@navbar-default-link-color:                #777;
-@navbar-default-link-hover-color:          #333;
-@navbar-default-link-hover-bg:             transparent;
-@navbar-default-link-active-color:         #555;
-@navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);
-@navbar-default-link-disabled-color:       #ccc;
-@navbar-default-link-disabled-bg:          transparent;
-
-// Navbar brand label
-@navbar-default-brand-color:               @navbar-default-link-color;
-@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);
-@navbar-default-brand-hover-bg:            transparent;
-
-// Navbar toggle
-@navbar-default-toggle-hover-bg:           #ddd;
-@navbar-default-toggle-icon-bar-bg:        #888;
-@navbar-default-toggle-border-color:       #ddd;
-
-
-//=== Inverted navbar
-// Reset inverted navbar basics
-@navbar-inverse-color:                      lighten(@gray-light, 15%);
-@navbar-inverse-bg:                         #222;
-@navbar-inverse-border:                     darken(@navbar-inverse-bg, 10%);
-
-// Inverted navbar links
-@navbar-inverse-link-color:                 lighten(@gray-light, 15%);
-@navbar-inverse-link-hover-color:           #fff;
-@navbar-inverse-link-hover-bg:              transparent;
-@navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;
-@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);
-@navbar-inverse-link-disabled-color:        #444;
-@navbar-inverse-link-disabled-bg:           transparent;
-
-// Inverted navbar brand label
-@navbar-inverse-brand-color:                @navbar-inverse-link-color;
-@navbar-inverse-brand-hover-color:          #fff;
-@navbar-inverse-brand-hover-bg:             transparent;
-
-// Inverted navbar toggle
-@navbar-inverse-toggle-hover-bg:            #333;
-@navbar-inverse-toggle-icon-bar-bg:         #fff;
-@navbar-inverse-toggle-border-color:        #333;
-
-
-//== Navs
-//
-//##
-
-//=== Shared nav styles
-@nav-link-padding:                          10px 15px;
-@nav-link-hover-bg:                         @gray-lighter;
-
-@nav-disabled-link-color:                   @gray-light;
-@nav-disabled-link-hover-color:             @gray-light;
-
-//== Tabs
-@nav-tabs-border-color:                     #ddd;
-
-@nav-tabs-link-hover-border-color:          @gray-lighter;
-
-@nav-tabs-active-link-hover-bg:             @body-bg;
-@nav-tabs-active-link-hover-color:          @gray;
-@nav-tabs-active-link-hover-border-color:   #ddd;
-
-@nav-tabs-justified-link-border-color:            #ddd;
-@nav-tabs-justified-active-link-border-color:     @body-bg;
-
-//== Pills
-@nav-pills-border-radius:                   @border-radius-base;
-@nav-pills-active-link-hover-bg:            @component-active-bg;
-@nav-pills-active-link-hover-color:         @component-active-color;
-
-
-//== Pagination
-//
-//##
-
-@pagination-color:                     @link-color;
-@pagination-bg:                        #fff;
-@pagination-border:                    #ddd;
-
-@pagination-hover-color:               @link-hover-color;
-@pagination-hover-bg:                  @gray-lighter;
-@pagination-hover-border:              #ddd;
-
-@pagination-active-color:              #fff;
-@pagination-active-bg:                 @brand-primary;
-@pagination-active-border:             @brand-primary;
-
-@pagination-disabled-color:            @gray-light;
-@pagination-disabled-bg:               #fff;
-@pagination-disabled-border:           #ddd;
-
-
-//== Pager
-//
-//##
-
-@pager-bg:                             @pagination-bg;
-@pager-border:                         @pagination-border;
-@pager-border-radius:                  15px;
-
-@pager-hover-bg:                       @pagination-hover-bg;
-
-@pager-active-bg:                      @pagination-active-bg;
-@pager-active-color:                   @pagination-active-color;
-
-@pager-disabled-color:                 @pagination-disabled-color;
-
-
-//== Jumbotron
-//
-//##
-
-@jumbotron-padding:              30px;
-@jumbotron-color:                inherit;
-@jumbotron-bg:                   @gray-lighter;
-@jumbotron-heading-color:        inherit;
-@jumbotron-font-size:            ceil((@font-size-base * 1.5));
-@jumbotron-heading-font-size:    ceil((@font-size-base * 4.5));
-
-
-//== Form states and alerts
-//
-//## Define colors for form feedback states and, by default, alerts.
-
-@state-success-text:             #3c763d;
-@state-success-bg:               #dff0d8;
-@state-success-border:           darken(spin(@state-success-bg, -10), 5%);
-
-@state-info-text:                #31708f;
-@state-info-bg:                  #d9edf7;
-@state-info-border:              darken(spin(@state-info-bg, -10), 7%);
-
-@state-warning-text:             #8a6d3b;
-@state-warning-bg:               #fcf8e3;
-@state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);
-
-@state-danger-text:              #a94442;
-@state-danger-bg:                #f2dede;
-@state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);
-
-
-//== Tooltips
-//
-//##
-
-//** Tooltip max width
-@tooltip-max-width:           200px;
-//** Tooltip text color
-@tooltip-color:               #fff;
-//** Tooltip background color
-@tooltip-bg:                  #000;
-@tooltip-opacity:             .9;
-
-//** Tooltip arrow width
-@tooltip-arrow-width:         5px;
-//** Tooltip arrow color
-@tooltip-arrow-color:         @tooltip-bg;
-
-
-//== Popovers
-//
-//##
-
-//** Popover body background color
-@popover-bg:                          #fff;
-//** Popover maximum width
-@popover-max-width:                   276px;
-//** Popover border color
-@popover-border-color:                rgba(0,0,0,.2);
-//** Popover fallback border color
-@popover-fallback-border-color:       #ccc;
-
-//** Popover title background color
-@popover-title-bg:                    darken(@popover-bg, 3%);
-
-//** Popover arrow width
-@popover-arrow-width:                 10px;
-//** Popover arrow color
-@popover-arrow-color:                 @popover-bg;
-
-//** Popover outer arrow width
-@popover-arrow-outer-width:           (@popover-arrow-width + 1);
-//** Popover outer arrow color
-@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);
-//** Popover outer arrow fallback color
-@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
-
-
-//== Labels
-//
-//##
-
-//** Default label background color
-@label-default-bg:            @gray-light;
-//** Primary label background color
-@label-primary-bg:            @brand-primary;
-//** Success label background color
-@label-success-bg:            @brand-success;
-//** Info label background color
-@label-info-bg:               @brand-info;
-//** Warning label background color
-@label-warning-bg:            @brand-warning;
-//** Danger label background color
-@label-danger-bg:             @brand-danger;
-
-//** Default label text color
-@label-color:                 #fff;
-//** Default text color of a linked label
-@label-link-hover-color:      #fff;
-
-
-//== Modals
-//
-//##
-
-//** Padding applied to the modal body
-@modal-inner-padding:         15px;
-
-//** Padding applied to the modal title
-@modal-title-padding:         15px;
-//** Modal title line-height
-@modal-title-line-height:     @line-height-base;
-
-//** Background color of modal content area
-@modal-content-bg:                             #fff;
-//** Modal content border color
-@modal-content-border-color:                   rgba(0,0,0,.2);
-//** Modal content border color **for IE8**
-@modal-content-fallback-border-color:          #999;
-
-//** Modal backdrop background color
-@modal-backdrop-bg:           #000;
-//** Modal backdrop opacity
-@modal-backdrop-opacity:      .5;
-//** Modal header border color
-@modal-header-border-color:   #e5e5e5;
-//** Modal footer border color
-@modal-footer-border-color:   @modal-header-border-color;
-
-@modal-lg:                    900px;
-@modal-md:                    600px;
-@modal-sm:                    300px;
-
-
-//== Alerts
-//
-//## Define alert colors, border radius, and padding.
-
-@alert-padding:               15px;
-@alert-border-radius:         @border-radius-base;
-@alert-link-font-weight:      bold;
-
-@alert-success-bg:            @state-success-bg;
-@alert-success-text:          @state-success-text;
-@alert-success-border:        @state-success-border;
-
-@alert-info-bg:               @state-info-bg;
-@alert-info-text:             @state-info-text;
-@alert-info-border:           @state-info-border;
-
-@alert-warning-bg:            @state-warning-bg;
-@alert-warning-text:          @state-warning-text;
-@alert-warning-border:        @state-warning-border;
-
-@alert-danger-bg:             @state-danger-bg;
-@alert-danger-text:           @state-danger-text;
-@alert-danger-border:         @state-danger-border;
-
-
-//== Progress bars
-//
-//##
-
-//** Background color of the whole progress component
-@progress-bg:                 fade(@body-bg, 20); //#f5f5f5;
-//** Progress bar text color
-@progress-bar-color:          @body-bg;
-//** Variable for setting rounded corners on progress bar.
-@progress-border-radius:      @border-radius-base;
-
-//** Default progress bar color
-@progress-bar-bg:             @PLAYING_NOW_COLOR;
-//** Success progress bar color
-@progress-bar-success-bg:     @brand-success;
-//** Warning progress bar color
-@progress-bar-warning-bg:     @brand-warning;
-//** Danger progress bar color
-@progress-bar-danger-bg:      @brand-danger;
-//** Info progress bar color
-@progress-bar-info-bg:        @brand-info;
-
-
-//== List group
-//
-//##
-
-//** Background color on `.list-group-item`
-@list-group-bg:                 #fff;
-//** `.list-group-item` border color
-@list-group-border:             #ddd;
-//** List group border radius
-@list-group-border-radius:      @border-radius-base;
-
-//** Background color of single list items on hover
-@list-group-hover-bg:           #f5f5f5;
-//** Text color of active list items
-@list-group-active-color:       @component-active-color;
-//** Background color of active list items
-@list-group-active-bg:          @component-active-bg;
-//** Border color of active list elements
-@list-group-active-border:      @list-group-active-bg;
-//** Text color for content within active list items
-@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
-
-//** Text color of disabled list items
-@list-group-disabled-color:      @gray-light;
-//** Background color of disabled list items
-@list-group-disabled-bg:         @gray-lighter;
-//** Text color for content within disabled list items
-@list-group-disabled-text-color: @list-group-disabled-color;
-
-@list-group-link-color:         #555;
-@list-group-link-hover-color:   @list-group-link-color;
-@list-group-link-heading-color: #333;
-
-
-//== Panels
-//
-//##
-
-@panel-bg:                    #fff;
-@panel-body-padding:          15px;
-@panel-heading-padding:       10px 15px;
-@panel-footer-padding:        @panel-heading-padding;
-@panel-border-radius:         @border-radius-base;
-
-//** Border color for elements within panels
-@panel-inner-border:          #ddd;
-@panel-footer-bg:             #f5f5f5;
-
-@panel-default-text:          @gray-dark;
-@panel-default-border:        #ddd;
-@panel-default-heading-bg:    #f5f5f5;
-
-@panel-primary-text:          #fff;
-@panel-primary-border:        @brand-primary;
-@panel-primary-heading-bg:    @brand-primary;
-
-@panel-success-text:          @state-success-text;
-@panel-success-border:        @state-success-border;
-@panel-success-heading-bg:    @state-success-bg;
-
-@panel-info-text:             @state-info-text;
-@panel-info-border:           @state-info-border;
-@panel-info-heading-bg:       @state-info-bg;
-
-@panel-warning-text:          @state-warning-text;
-@panel-warning-border:        @state-warning-border;
-@panel-warning-heading-bg:    @state-warning-bg;
-
-@panel-danger-text:           @state-danger-text;
-@panel-danger-border:         @state-danger-border;
-@panel-danger-heading-bg:     @state-danger-bg;
-
-
-//== Thumbnails
-//
-//##
-
-//** Padding around the thumbnail image
-@thumbnail-padding:           4px;
-//** Thumbnail background color
-@thumbnail-bg:                @body-bg;
-//** Thumbnail border color
-@thumbnail-border:            #ddd;
-//** Thumbnail border radius
-@thumbnail-border-radius:     @border-radius-base;
-
-//** Custom text color for thumbnail captions
-@thumbnail-caption-color:     @text-color;
-//** Padding around the thumbnail caption
-@thumbnail-caption-padding:   9px;
-
-
-//== Wells
-//
-//##
-
-@well-bg:                     #f5f5f5;
-@well-border:                 darken(@well-bg, 7%);
-
-
-//== Badges
-//
-//##
-
-@badge-color:                 #fff;
-//** Linked badge text color on hover
-@badge-link-hover-color:      #fff;
-@badge-bg:                    @gray-light;
-
-//** Badge text color in active nav link
-@badge-active-color:          @link-color;
-//** Badge background color in active nav link
-@badge-active-bg:             #fff;
-
-@badge-font-weight:           bold;
-@badge-line-height:           1;
-@badge-border-radius:         10px;
-
-
-//== Breadcrumbs
-//
-//##
-
-@breadcrumb-padding-vertical:   8px;
-@breadcrumb-padding-horizontal: 15px;
-//** Breadcrumb background color
-@breadcrumb-bg:                 #f5f5f5;
-//** Breadcrumb text color
-@breadcrumb-color:              #ccc;
-//** Text color of current page in the breadcrumb
-@breadcrumb-active-color:       @gray-light;
-//** Textual separator for between breadcrumb elements
-@breadcrumb-separator:          "/";
-
-
-//== Carousel
-//
-//##
-
-@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
-
-@carousel-control-color:                      #fff;
-@carousel-control-width:                      15%;
-@carousel-control-opacity:                    .5;
-@carousel-control-font-size:                  20px;
-
-@carousel-indicator-active-bg:                #fff;
-@carousel-indicator-border-color:             #fff;
-
-@carousel-caption-color:                      #fff;
-
-
-//== Close
-//
-//##
-
-@close-font-weight:           bold;
-@close-color:                 #000;
-@close-text-shadow:           0 1px 0 #fff;
-
-
-//== Code
-//
-//##
-
-@code-color:                  #c7254e;
-@code-bg:                     #f9f2f4;
-
-@kbd-color:                   #fff;
-@kbd-bg:                      #333;
-
-@pre-bg:                      #f5f5f5;
-@pre-color:                   @gray-dark;
-@pre-border-color:            #ccc;
-@pre-scrollable-max-height:   340px;
-
-
-//== Type
-//
-//##
-
-//** Horizontal offset for forms and lists.
-@component-offset-horizontal: 180px;
-//** Text muted color
-@text-muted:                  @gray-light;
-//** Abbreviations and acronyms border color
-@abbr-border-color:           @gray-light;
-//** Headings small color
-@headings-small-color:        @gray-light;
-//** Blockquote small color
-@blockquote-small-color:      @gray-light;
-//** Blockquote font size
-@blockquote-font-size:        (@font-size-base * 1.25);
-//** Blockquote border color
-@blockquote-border-color:     @gray-lighter;
-//** Page header border color
-@page-header-border-color:    @gray-lighter;
-//** Width of horizontal description list titles
-@dl-horizontal-offset:        @component-offset-horizontal;
-//** Point at which .dl-horizontal becomes horizontal
-@dl-horizontal-breakpoint:    @grid-float-breakpoint;
-//** Horizontal line color.
-@hr-border:                   @gray-lighter;
diff --git a/app/l8pr/templates/index.html b/app/l8pr/templates/index.html
deleted file mode 100644
index 8061ff7..0000000
--- a/app/l8pr/templates/index.html
+++ /dev/null
@@ -1,90 +0,0 @@
-{% load staticfiles %}
-{% load compress %}
-
-
-    
-        
-        
-        
-        {% comment %}
-        
-        {% endcomment %}
-        
-        
-        
-        
-        
-        
-        {% compress css %}
-        
-        
-        
-        
-        {% endcompress %}
-        
-        
-        
-        
-    
-    
-        {% csrf_token %}
-        
- - {% compress js %} - - {% endfor %} - {% if GA %} - - {% endif %} - - diff --git a/app/l8pr/views.py b/app/l8pr/views.py index d5f93b0..06e4f9a 100644 --- a/app/l8pr/views.py +++ b/app/l8pr/views.py @@ -7,6 +7,8 @@ from django.core.exceptions import ObjectDoesNotExist from django.views.decorators.clickjacking import xframe_options_exempt from django.utils.decorators import method_decorator +from django.views.generic import View +from django.http import HttpResponse def angular_templates(): @@ -22,6 +24,16 @@ def angular_templates(): yield (file_name, normalize_newlines(fh.read().decode('utf-8')).replace('\n', ' ')) +class IndexView(View): + """Render main page.""" + + def get(self, request): + """Return html for main application page.""" + + abspath = open(os.path.join(settings.BASE_DIR, 'static_dist/index.html'), 'r') + return HttpResponse(content=abspath.read()) + + @method_decorator(xframe_options_exempt, name='dispatch') class HomePageView(TemplateView): diff --git a/app/settings.py b/app/settings.py index 5d3b61a..8532794 100644 --- a/app/settings.py +++ b/app/settings.py @@ -37,10 +37,11 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'compressor', + 'webpack_loader', + # 'compressor', 'rest_framework', 'oauth2_provider', - 'social.apps.django_app.default', + # 'social.apps.django_app.default', 'rest_framework_social_oauth2', 'djoser', 'haystack', @@ -72,8 +73,8 @@ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - 'social.apps.django_app.context_processors.backends', - 'social.apps.django_app.context_processors.login_redirect', + # 'social.apps.django_app.context_processors.backends', + # 'social.apps.django_app.context_processors.login_redirect', ], }, }, @@ -147,9 +148,9 @@ 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'compressor.finders.CompressorFinder', ) -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'app', 'l8pr', 'static'), -] +STATICFILES_DIRS = ( + os.path.join(BASE_DIR, 'static_dist'), +) COMPRESS_ENABLED = str(os.environ.get('COMPRESS_ENABLED', not DEBUG)).lower() == 'true' COMPRESS_OFFLINE = str(os.environ.get('COMPRESS_OFFLINE', not DEBUG)).lower() == 'true' COMPRESS_PRECOMPILERS = ( @@ -185,9 +186,6 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' AUTHENTICATION_BACKENDS = ( # Facebook OAuth2 - 'social.backends.facebook.Facebook2OAuth2', - 'social.backends.facebook.FacebookAppOAuth2', - 'social.backends.facebook.FacebookOAuth2', # django-rest-framework-social-oauth2 'rest_framework_social_oauth2.backends.DjangoOAuth2', # Django diff --git a/app/urls.py b/app/urls.py index eca5051..fca21cd 100644 --- a/app/urls.py +++ b/app/urls.py @@ -19,7 +19,7 @@ from rest_framework import routers from .l8pr.api import (UserViewSet, LoopViewSet, ShowViewSet, ItemViewSet, ItemSearchView, SearchYoutubeView, MetadataView) -from .l8pr.views import HomePageView +from .l8pr.views import HomePageView, IndexView from django.contrib import admin from django.conf.urls.static import static from django.conf import settings @@ -44,11 +44,12 @@ url(r'^api/register/', include('djoser.urls')), url(r'^auth/', include('rest_framework_social_oauth2.urls')), url(r'^admin/', admin.site.urls), - url(r'^$', HomePageView.as_view(template_name='index.html')), url(r'^open/.+$', HomePageView.as_view(template_name='index.html')), - url(r'^(?P\w+)$', HomePageView.as_view(template_name='index.html')), - url(r'^(?P\w+)/.*$', HomePageView.as_view(template_name='index.html')), + # url(r'^(?P\w+)$', HomePageView.as_view(template_name='index.html')), + # url(r'^(?P\w+)/.*$', HomePageView.as_view(template_name='index.html')), # from FB auth - url(r'^_=_/$', HomePageView.as_view(template_name='index.html')), + url(r'', IndexView.as_view(), name='index'), + + # url(r'^_=_/$', HomePageView.as_view(template_name='index.html')), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + \ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/package.json b/package.json index 0fe2113..bf4cac4 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,111 @@ { - "name": "L8pr", + "name": "Loopr", "version": "1.0.0", - "description": "", - "main": "index.js", + "description": "Django, React and Redux!", + "scripts": { + "dev": "webpack --progress --display-error-details --config webpack/common.config.js --watch", + "prod": "webpack --progress -p --config webpack/common.config.js", + "mocha": "mocha --require tests/require --recursive --compilers js:babel-core/register tests/js", + "mocha:watch": "npm run mocha -- --watch tests/js", + "coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --require tests/require --recursive --compilers js:babel-core/register --colors --reporter dot tests/js/", + "lintjs": "eslint -c .eslintrc src/static tests/js || true", + "lintscss": "sass-lint --verbose --no-exit || true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Seedstars/django-react-redux-base.git" + }, + "author": "Luis Rodrigues ", + "license": "MIT", "dependencies": { - "bower": "latest", - "less": "^2.5.1" + "autoprefixer": "6.5.4", + "babel-core": "6.21.0", + "babel-loader": "6.2.10", + "babel-plugin-react-display-name": "2.0.0", + "babel-plugin-react-transform": "2.0.2", + "babel-plugin-transform-decorators-legacy": "1.3.4", + "babel-plugin-undeclared-variables-check": "6.8.0", + "babel-polyfill": "6.20.0", + "babel-preset-es2015": "6.18.0", + "babel-preset-es2015-loose": "8.0.0", + "babel-preset-react": "6.16.0", + "babel-preset-stage-1": "6.16.0", + "babel-runtime": "6.20.0", + "body-parser": "1.15.2", + "bootstrap-loader": "2.0.0-beta.16", + "bootstrap-sass": "3.3.7", + "classnames": "2.2.5", + "css-loader": "0.26.1", + "dotenv": "2.0.0", + "errorhandler": "1.5.0", + "es6-promise": "4.0.5", + "exports-loader": "0.6.3", + "extract-text-webpack-plugin": "2.0.0-beta.4", + "file-loader": "0.9.0", + "font-awesome": "4.7.0", + "font-awesome-webpack": "0.0.4", + "history": "4.5.0", + "html-webpack-plugin": "2.24.1", + "imports-loader": "0.7.0", + "isomorphic-fetch": "2.2.1", + "jquery": "3.1.1", + "less": "2.7.1", + "less-loader": "2.2.3", + "material-design-icons": "^3.0.1", + "method-override": "2.3.7", + "node-sass": "3.13.1", + "postcss-import": "9.0.0", + "postcss-loader": "1.2.0", + "react": "15.4.1", + "react-dom": "15.4.1", + "react-mixin": "3.0.5", + "react-player": "^0.14.3", + "react-redux": "4.4.6", + "react-router": "3.0.0", + "react-router-redux": "4.0.7", + "redux": "3.6.0", + "redux-thunk": "2.1.0", + "reselect": "^3.0.0", + "resolve-url-loader": "1.6.1", + "sass-loader": "4.1.0", + "style-loader": "0.13.1", + "tcomb-form": "0.9.10", + "url-loader": "0.5.7", + "webpack": "2.2.0-rc.1", + "webpack-merge": "1.1.1", + "whatwg-fetch": "2.0.1", + "yargs": "6.5.0" }, - "devDependencies": {}, - "scripts": {}, - "author": "", - "license": "ISC" + "devDependencies": { + "babel-eslint": "7.1.1", + "chai": "3.5.0", + "chai-as-promised": "6.0.0", + "clean-webpack-plugin": "0.1.14", + "enzyme": "2.6.0", + "eslint": "3.12.2", + "eslint-config-airbnb": "13.0.0", + "eslint-plugin-import": "2.2.0", + "eslint-plugin-jsx-a11y": "2.2.3", + "eslint-plugin-react": "6.8.0", + "expect": "1.20.2", + "ignore-styles": "5.0.01", + "istanbul": "1.0.0-alpha.2", + "jsdom": "9.8.3", + "mocha": "3.2.0", + "mocha-junit-reporter": "1.12.1", + "nock": "9.0.2", + "react-addons-test-utils": "15.4.1", + "react-transform-catch-errors": "1.0.2", + "react-transform-hmr": "1.0.4", + "redbox-react": "1.3.3", + "redux-devtools": "3.3.1", + "redux-devtools-dock-monitor": "1.1.1", + "redux-devtools-log-monitor": "1.1.1", + "redux-logger": "2.7.4", + "redux-mock-store": "1.2.1", + "sass-lint": "1.10.2", + "sinon": "1.17.6", + "webpack-dev-middleware": "1.9.0", + "webpack-hot-middleware": "2.13.2" + } } diff --git a/protractor.js b/protractor.js deleted file mode 100644 index 3707866..0000000 --- a/protractor.js +++ /dev/null @@ -1,10 +0,0 @@ -(function() { -'use strict'; -exports.config = { - framework: 'jasmine', - seleniumAddress: 'http://web:4444/wd/hub', - chromeOnly: true, - directConnect: true, - specs: ['specs/*_spec.js'] -}; -})(); diff --git a/specs/show_spec.js b/specs/show_spec.js deleted file mode 100644 index 1b71ef6..0000000 --- a/specs/show_spec.js +++ /dev/null @@ -1,102 +0,0 @@ -// spec.js -(function() { - 'use strict'; - - function login() { - element(by.css('[ng-click="vm.openLoginView()"]')).click(); - element(by.id('inputUsername')).sendKeys('vied12'); - element(by.id('inputPassword')).sendKeys('pouet'); - browser.waitForAngular(); - element(by.css('[form="loginForm"]')).click(); - browser.waitForAngular(); - } - beforeEach(function() { - browser.get('/vied12'); - browser.waitForAngular(); - element(by.css('.toggle-controller')).click(); - browser.waitForAngular(); - jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; - login(); - }); - afterEach(function() { - browser.driver.get(browser.baseUrl + '/api/auth/logout/'); - }); - function openShow(show) { - element(by.cssContainingText('[ui-sref="index.open.show({showToExplore: show})"]', show)).click(); - } - describe('Show', function() { - var items = element.all(by.repeater('item in vm.show.items')); - it('should remove an item from a show', function() { - openShow('SLWX'); - items.count().then(function(initialCount) { - // remove - items.get(0).element(by.css('[ng-click="vm.removeItem(item)"]')).click(); - // confirm - element(by.css('[ng-click="ok()"]')).click(); - // check - expect(items.count()).toEqual(initialCount - 1); - // reload and check again - browser.refresh(); - expect(items.count()).toEqual(initialCount - 1); - }); - }); - it('should add an item to a show', function() { - element(by.css('.banner__title')).getText().then(function(itemTitle) { - element(by.css('.banner [ng-click="vm.addToShowModal(vm.Player.currentItem)"]')).click(); - var shows = element.all(by.repeater('show in vm.shows')); - shows.get(0).element(by.css('.list__item__title')).getText().then(function(showName) { - shows.get(0).click(); - browser.get('/vied12/loop/'); - openShow(showName); - expect(element(by.css('.list__item__title', itemTitle)).isPresent()).toBe(true); - }); - }); - }); - xit('search an url and add it', function() { - element(by.css('[ng-click="vm.searchBarVisible = !vm.searchBarVisible"]')).click(); - element(by.model('vm.searchQuery')) - .sendKeys('https://www.youtube.com/watch?v=dYHAFRtrL-Q') - .sendKeys(protractor.Key.ENTER); - element.all(by.repeater('result in vm.getResults()')).get(0) - .element(by.css('[ng-click="$event.stopPropagation(); vm.addItemToAShow(result);"]')) - .click(); - var shows = element.all(by.repeater('show in vm.shows')); - shows.get(0).element(by.css('.list__item__title')).getText().then(function(showName) { - shows.get(0).click(); - browser.waitForAngular(); - browser.get('/vied12/loop/'); - browser.waitForAngular(); - openShow(showName); - expect(element(by.css('.list__item__title', 'Best Of - Alexandre Astier')).isPresent()).toBe(true); - }); - }); - }); - describe('Open', function() { - function openUrlInLoopr(url, title) { - browser.get('/open/' + encodeURIComponent(url)); - browser.waitForAngular(); - browser.sleep(2000); - element(by.css('.banner__title')).getText().then(function(itemTitle) { - expect(itemTitle).toBe(title); - element(by.css('.toggle-controller')).click(); - element(by.css('.banner [ng-click="vm.addToShowModal(vm.Player.currentItem)"]')).click(); - var shows = element.all(by.repeater('show in vm.shows')); - shows.get(1).element(by.css('.list__item__title')).getText().then(function(showName) { - shows.get(1).click(); - browser.get('/vied12/loop/'); - openShow(showName); - expect(element(by.css('.list__item__title', itemTitle)).isPresent()).toBe(true); - element(by.css('[ui-sref="index.open.loop"]')).click(); - openShow('my inbox'); - expect(element(by.css('.list__item__title', itemTitle)).isPresent()).toBe(true); - }); - }); - } - it('open an url with loopr.tv (firefox extension)', function() { - openUrlInLoopr('https://www.youtube.com/watch?v=sSrXhylmIQc', - 'Top 10 Science Experiments - Experiments You Can Do at Home Compilation'); - openUrlInLoopr('https://www.youtube.com/watch?v=05E-mtVbMIE', - '6 Science Tricks - Amazing Experiments'); - }); - }); -})(); diff --git a/webpack/common.config.js b/webpack/common.config.js new file mode 100644 index 0000000..ee7244d --- /dev/null +++ b/webpack/common.config.js @@ -0,0 +1,152 @@ +const path = require('path'); +const autoprefixer = require('autoprefixer'); +const merge = require('webpack-merge'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const webpack = require('webpack'); +const CleanWebpackPlugin = require('clean-webpack-plugin'); + +const TARGET = process.env.npm_lifecycle_event; + +const PATHS = { + app: path.join(__dirname, '../app/l8pr/static'), + build: path.join(__dirname, '../static_dist'), +}; + +const VENDOR = [ + 'babel-polyfill', + 'history', + 'react', + 'react-dom', + 'react-redux', + 'react-router', + 'react-mixin', + 'classnames', + 'redux', + 'react-router-redux', + // 'jquery', + 'bootstrap-loader', + 'font-awesome-webpack!./styles/font-awesome.config.prod.js' +]; + +const basePath = path.resolve(__dirname, '../app/l8pr/static/'); + +const common = { + context: basePath, + entry: { + vendor: VENDOR, + app: PATHS.app + }, + output: { + filename: '[name].[hash].js', + path: PATHS.build, + publicPath: '/static' + }, + plugins: [ + // extract all common modules to vendor so we can load multiple apps in one page + // new webpack.optimize.CommonsChunkPlugin({ + // name: 'vendor', + // filename: 'vendor.[hash].js' + // }), + new webpack.optimize.CommonsChunkPlugin({ + children: true, + async: true, + minChunks: 2 + }), + new webpack.LoaderOptionsPlugin({ + test: /\.scss$/, + options: { + postcss: [ + autoprefixer({ browsers: ['last 2 versions'] }) + ], + sassLoader: { + data: `@import "${__dirname}/../app/l8pr/static/styles/config/_variables.scss";` + } + } + }), + new webpack.LoaderOptionsPlugin({ + test: /\.css$/, + options: { + postcss: [ + autoprefixer({ browsers: ['last 2 versions'] }) + ] + } + }), + new HtmlWebpackPlugin({ + template: path.join(__dirname, '../app/l8pr/static/index.html'), + hash: true, + filename: 'index.html', + inject: 'body' + }), + new webpack.DefinePlugin({ + 'process.env': { NODE_ENV: TARGET === 'dev' ? '"development"' : '"production"' }, + '__DEVELOPMENT__': TARGET === 'dev' + }), + new webpack.ProvidePlugin({ + '$': 'jquery', + 'jQuery': 'jquery', + 'window.jQuery': 'jquery' + }), + new CleanWebpackPlugin([PATHS.build], { + root: process.cwd() + }) + ], + resolve: { + extensions: ['.jsx', '.js'], + // extensions: ['.jsx', '.js', '.json', '.scss', '.css'], + modules: ['node_modules'] + }, + module: { + rules: [ + { + test: /\.js$/, + use: [ + { loader: 'babel-loader' } + ], + exclude: /node_modules/ + }, + { + test: /\.jpe?g$|\.gif$|\.png$/, + loader: 'file-loader?name=/images/[name].[ext]?[hash]' + }, + { + test: /\.woff(\?.*)?$/, + loader: 'url-loader?name=/fonts/[name].[ext]&limit=10000&mimetype=application/font-woff' + }, + { + test: /\.woff2(\?.*)?$/, + loader: 'url-loader?name=/fonts/[name].[ext]&limit=10000&mimetype=application/font-woff2' + }, + { + test: /\.ttf(\?.*)?$/, + loader: 'url-loader?name=/fonts/[name].[ext]&limit=10000&mimetype=application/octet-stream' + }, + { + test: /\.eot(\?.*)?$/, + loader: 'file-loader?name=/fonts/[name].[ext]' + }, + { + test: /\.otf(\?.*)?$/, + loader: 'file-loader?name=/fonts/[name].[ext]&mimetype=application/font-otf' + }, + { + test: /\.svg(\?.*)?$/, + loader: 'url-loader?name=/fonts/[name].[ext]&limit=10000&mimetype=image/svg+xml' + }, + { + test: /\.json(\?.*)?$/, + loader: 'file-loader?name=/files/[name].[ext]' + } + ] + }, +}; + +switch (TARGET) { + case 'dev': + module.exports = merge(require('./dev.config'), common); + break; + case 'prod': + module.exports = merge(require('./prod.config'), common); + break; + default: + console.log('Target configuration not found. Valid targets: "dev" or "prod".'); +} diff --git a/webpack/dev.config.js b/webpack/dev.config.js new file mode 100644 index 0000000..aa3489d --- /dev/null +++ b/webpack/dev.config.js @@ -0,0 +1,29 @@ +const webpack = require('webpack'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +const extractCSS = new ExtractTextPlugin('styles/[name].css'); + +module.exports = { + devtool: 'source-map', // 'cheap-module-eval-source-map' + module: { + rules: [{ + test: /\.css$/, + use: [ + extractCSS.extract('style'), + 'css-loader?localIdentName=[path][name]--[local]', + 'postcss-loader' + ] + }, { + test: /\.scss$/, + use: [ + extractCSS.extract('style'), + 'css-loader?localIdentName=[path][name]--[local]', + 'postcss-loader', + 'sass-loader', + ] + }], + }, + plugins: [ + extractCSS + ] +}; diff --git a/webpack/prod.config.js b/webpack/prod.config.js new file mode 100644 index 0000000..2d5b134 --- /dev/null +++ b/webpack/prod.config.js @@ -0,0 +1,37 @@ +const webpack = require('webpack'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +const extractCSS = new ExtractTextPlugin('styles/[name].css'); + +module.exports = { + // devtool: 'source-map', // No need for dev tool in production + + module: { + rules: [{ + test: /\.css$/, + use: [ + extractCSS.extract('style'), + 'css-loader?localIdentName=[path][name]--[local]', + 'postcss-loader' + ] + }, { + test: /\.scss$/, + use: [ + extractCSS.extract('style'), + 'css-loader?localIdentName=[path][name]--[local]', + 'postcss-loader', + 'sass-loader', + ] + }], + }, + + plugins: [ + extractCSS, + new webpack.optimize.OccurrenceOrderPlugin(), + new webpack.optimize.UglifyJsPlugin({ + compress: { + warnings: false + } + }) + ] +}; From 0e31c325002e548dd210224bf0c1150f0f6a071b Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Wed, 5 Apr 2017 16:28:46 +0200 Subject: [PATCH 003/143] play latest items --- app/l8pr/admin.py | 22 +++++++++++- app/l8pr/api.py | 2 +- .../migrations/0023_auto_20170405_1342.py | 36 +++++++++++++++++++ app/l8pr/models.py | 14 ++++++++ app/l8pr/static/actions/browser.js | 2 +- app/l8pr/static/actions/data.js | 14 ++++++++ app/l8pr/static/actions/player.js | 12 ++++++- app/l8pr/static/components/NavPlayer/index.js | 15 ++++++-- app/l8pr/static/components/Screen/index.js | 1 - .../static/containers/Controller/index.js | 11 ++++-- app/l8pr/static/containers/Home/index.js | 21 ++++++----- app/l8pr/static/reducers/player.js | 7 +--- app/l8pr/static/selectors.js | 16 ++++----- 13 files changed, 140 insertions(+), 33 deletions(-) create mode 100644 app/l8pr/migrations/0023_auto_20170405_1342.py diff --git a/app/l8pr/admin.py b/app/l8pr/admin.py index a1994ef..a7f980b 100644 --- a/app/l8pr/admin.py +++ b/app/l8pr/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Loop, Show, Item, ShowsRelationship, ItemsRelationship, ShowSettings, Profile +from .models import Loop, Show, Item, ShowsRelationship, ItemsRelationship, ItemsUsersRelationship, ShowSettings, Profile from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User @@ -20,10 +20,15 @@ class ItemsRelationshipInline(admin.TabularInline): model = Show.items.through +class ItemsUsersRelationshipInline(admin.TabularInline): + model = Item.users.through + + class LoopAdmin(admin.ModelAdmin): list_display = ('__str__', 'active') inlines = (ShowsRelationshipInline,) + admin.site.register(Loop, LoopAdmin) @@ -31,24 +36,39 @@ class ShowAdmin(admin.ModelAdmin): list_display = ('__str__', 'user', 'added', 'updated') inlines = (SettingsInline, ItemsRelationshipInline,) + admin.site.register(Show, ShowAdmin) class ItemAdmin(admin.ModelAdmin): list_display = ('__str__', 'title', 'author_name', 'provider_name', 'duration') + inlines = (ItemsUsersRelationshipInline,) + + admin.site.register(Item, ItemAdmin) class ShowsRelationshipAdmin(admin.ModelAdmin): pass + + admin.site.register(ShowsRelationship, ShowsRelationshipAdmin) class ItemsRelationshipAdmin(admin.ModelAdmin): pass + + admin.site.register(ItemsRelationship, ItemsRelationshipAdmin) +class ItemsUsersRelationshipAdmin(admin.ModelAdmin): + pass + + +admin.site.register(ItemsUsersRelationship, ItemsUsersRelationshipAdmin) + + class ProfileInline(admin.StackedInline): model = Profile diff --git a/app/l8pr/api.py b/app/l8pr/api.py index dc7f60d..d1d7e92 100644 --- a/app/l8pr/api.py +++ b/app/l8pr/api.py @@ -180,7 +180,7 @@ class ShowViewSet(viewsets.ModelViewSet): class ItemViewSet(viewsets.ModelViewSet): queryset = Item.objects.all() serializer_class = ItemSerializer - filter_fields = ('url',) + filter_fields = ('url', 'users') class ItemSearchSerializer(HaystackSerializer): diff --git a/app/l8pr/migrations/0023_auto_20170405_1342.py b/app/l8pr/migrations/0023_auto_20170405_1342.py new file mode 100644 index 0000000..eed83f9 --- /dev/null +++ b/app/l8pr/migrations/0023_auto_20170405_1342.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-05 13:42 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('l8pr', '0022_show_show_type'), + ] + + operations = [ + migrations.CreateModel( + name='ItemsUsersRelationship', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('added', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='l8pr.Item')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-added',), + }, + ), + migrations.AddField( + model_name='item', + name='users', + field=models.ManyToManyField(related_name='ItemsUsersRelationship', through='l8pr.ItemsUsersRelationship', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/app/l8pr/models.py b/app/l8pr/models.py index c3ca9e5..7d3d1a9 100644 --- a/app/l8pr/models.py +++ b/app/l8pr/models.py @@ -111,6 +111,19 @@ def __str__(self): return '%s > %s' % (self.item, self.show) +class ItemsUsersRelationship(models.Model): + item = models.ForeignKey('Item') + user = models.ForeignKey(User) + added = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ('-added',) + + def __str__(self): + return '%s > %s' % (self.item, self.user) + + class Item(models.Model): PROVIDER_CHOICES = ( ('YouTube', 'YouTube'), @@ -118,6 +131,7 @@ class Item(models.Model): ('WebTorrent', 'WebTorrent'), ('Vimeo', 'Vimeo'), ) + users = models.ManyToManyField(User, through='ItemsUsersRelationship', related_name='ItemsUsersRelationship') title = models.CharField(max_length=255, null=True, blank=True) description = models.TextField(null=True, blank=True) author_name = models.CharField(max_length=255, null=True, blank=True) diff --git a/app/l8pr/static/actions/browser.js b/app/l8pr/static/actions/browser.js index de37052..1e7d2b2 100644 --- a/app/l8pr/static/actions/browser.js +++ b/app/l8pr/static/actions/browser.js @@ -34,7 +34,7 @@ function receiveLoop(loop) { } } -export function fetchLoop(userId=2) { +export function fetchShows(userId=2) { return (dispatch) => ( fetch(`${SERVER_URL}/api/loops/${userId}/`, { // credentials: 'include', diff --git a/app/l8pr/static/actions/data.js b/app/l8pr/static/actions/data.js index 22ab2b3..5988b34 100644 --- a/app/l8pr/static/actions/data.js +++ b/app/l8pr/static/actions/data.js @@ -6,6 +6,20 @@ import { checkHttpStatus, parseJSON } from '../utils'; import { DATA_FETCH_PROTECTED_DATA_REQUEST, DATA_RECEIVE_PROTECTED_DATA, RECEIVE_LOOP } from '../constants'; import { authLoginUserFailure } from './auth'; +export function fetchLastItems({user}) { + return (dispatch) => ( + fetch(`${SERVER_URL}/api/items/?users=${user}`, { + // credentials: 'include', + headers: { + Accept: 'application/json', + // Authorization: `Token ${token}` + } + }) + .then(checkHttpStatus) + .then(parseJSON) + ) +} + export function dataReceiveProtectedData(data) { return { type: DATA_RECEIVE_PROTECTED_DATA, diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index a4aa0c0..23d6472 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -21,7 +21,13 @@ export function play(showAndItem) { }) const show = selectors.currentShow(getState()) const item = selectors.currentTrack(getState()) - dispatch(push(`/show/${show}/item/${item}`)) + let url = '' + if (show) { + url += `/show/${show}` + } if (item) { + url += `/item/${item}` + } + dispatch(push(url)) } } @@ -49,3 +55,7 @@ export function next() { export function previous() { return { type: c.PREVIOUS } } + +export function pause() { + return { type: c.PAUSE } +} diff --git a/app/l8pr/static/components/NavPlayer/index.js b/app/l8pr/static/components/NavPlayer/index.js index c4a51f4..1614929 100644 --- a/app/l8pr/static/components/NavPlayer/index.js +++ b/app/l8pr/static/components/NavPlayer/index.js @@ -5,9 +5,11 @@ export default function NavPlayer({ onPreviousShow, onPreviousItem, onPlay, + onPause, onNextItem, onNextShow, onMute, + playing, }) { return (
@@ -18,9 +20,16 @@ export default function NavPlayer({ chevron_left - - play_arrow - + {playing && + + pause + + } + {!playing && + + play_arrow + + } chevron_right diff --git a/app/l8pr/static/components/Screen/index.js b/app/l8pr/static/components/Screen/index.js index ca689a5..15c5bac 100644 --- a/app/l8pr/static/components/Screen/index.js +++ b/app/l8pr/static/components/Screen/index.js @@ -8,7 +8,6 @@ export default class Screen extends React.Component { return ( diff --git a/app/l8pr/static/containers/Controller/index.js b/app/l8pr/static/containers/Controller/index.js index 3bf382b..4d5b3c4 100644 --- a/app/l8pr/static/containers/Controller/index.js +++ b/app/l8pr/static/containers/Controller/index.js @@ -1,14 +1,21 @@ import React from 'react' import { connect } from 'react-redux' import NavPlayer from '../../components/NavPlayer' -import { next } from '../../actions/player' +import { next, play, pause } from '../../actions/player' function ControllerComponent(props) { return ( ) } + +const mapStateToProps = (state) => ({ + playing: state.player.playing, +}) + const mapDispatchToProps = (dispatch) => ({ onNextItem: () => (dispatch(next())), + onPlay: () => (dispatch(play())), + onPause: () => (dispatch(pause())), }) -export default connect(null, mapDispatchToProps)(ControllerComponent) +export default connect(mapStateToProps, mapDispatchToProps)(ControllerComponent) diff --git a/app/l8pr/static/containers/Home/index.js b/app/l8pr/static/containers/Home/index.js index 7c49cce..c029b91 100644 --- a/app/l8pr/static/containers/Home/index.js +++ b/app/l8pr/static/containers/Home/index.js @@ -3,16 +3,16 @@ import { Link } from 'react-router' import { connect } from 'react-redux' import { Screen } from '../../components' import { Strip } from '../index' -import { fetchLoop } from '../../actions/browser' -import { setPlaylist, play, next } from '../../actions/player' +import { fetchLastItems } from '../../actions/data' +import { setPlaylist, play, pause, next } from '../../actions/player' import * as selectors from '../../selectors' import './style.scss' class HomeView extends React.Component { componentWillMount() { - this.props.fetchLoop(this.props.location) - .then((loop) => this.props.setPlaylist(loop)) + this.props.fetchLastItems({ user: 1 }) + .then((items) => this.props.setPlaylist(items)) .then(() => this.props.play(this.props.location)) } @@ -26,7 +26,10 @@ class HomeView extends React.Component { {this.props.media && } @@ -35,18 +38,18 @@ class HomeView extends React.Component { } } -const mapStateToProps = (state) => { - return { +const mapStateToProps = (state) => ({ media: selectors.getCurrentTrack(state), location: selectors.getLocation(state), - } -} + playing: state.player.playing, +}) const mapDispatchToProps = (dispatch) => ({ nextItem: () => (dispatch(next())), - fetchLoop: () => (dispatch(fetchLoop())), + fetchLastItems: (d) => (dispatch(fetchLastItems(d))), setPlaylist: (loop) => (dispatch(setPlaylist(loop))), play: (args) => (dispatch(play(args))), + pause: (args) => (dispatch(pause(args))), }) export default connect(mapStateToProps, mapDispatchToProps)(HomeView) diff --git a/app/l8pr/static/reducers/player.js b/app/l8pr/static/reducers/player.js index 66dd12f..097cdf2 100644 --- a/app/l8pr/static/reducers/player.js +++ b/app/l8pr/static/reducers/player.js @@ -3,8 +3,7 @@ import { get } from 'lodash' export default function (state = { playlist: [], - currentTrack: undefined, - currentShow: undefined, + currentTrack: 0, muted: false, playing: false }, action = null) { @@ -15,13 +14,9 @@ export default function (state = { playlist: action.payload, } case c.PLAY: - var showId = get(action, 'payload.show') || state.currentShow || state.playlist[0].id - var itemId = get(action, 'payload.item') || state.currentTrack || state.playlist[0].items[0].id return { ...state, playing: true, - currentShow: parseInt(showId), - currentTrack: parseInt(itemId), } case c.PAUSE: return { ...state, playing: false } diff --git a/app/l8pr/static/selectors.js b/app/l8pr/static/selectors.js index 5d42c05..64c5469 100644 --- a/app/l8pr/static/selectors.js +++ b/app/l8pr/static/selectors.js @@ -5,6 +5,14 @@ export const currentTrack = (state) => state.player.currentTrack export const currentShow = (state) => state.player.currentShow export const playlist = (state) => state.player.playlist export const getPathname = (state) => state.routing.locationBeforeTransitions.pathname + +export const getCurrentTrack = createSelector( + [playlist, currentTrack], + (playlist, currentTrackPosition) => { + return playlist[currentTrackPosition] + } +) + export const getCurrentShow = createSelector( [currentShow, playlist], (currentShowId, playlist) => { @@ -12,14 +20,6 @@ export const getCurrentShow = createSelector( } ) -export const getCurrentTrack = createSelector( - [getCurrentShow, currentTrack], - (currentShow, currentTrackId) => { - if (currentShow) { - return currentShow.items.find((i) => i.id === currentTrackId) - } - } -) export const getCurrentShowPositionInShow = createSelector( [currentShow, playlist], From 96d602f3a81613bedc01e030dacf1a31d3db6327 Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Sat, 8 Apr 2017 17:26:47 +0200 Subject: [PATCH 004/143] init queue --- .eslintrc.js | 73 +++++++++++++++ app/l8pr/static/actions/data.js | 88 ++++++++++--------- app/l8pr/static/actions/player.js | 80 ++++++++++++----- app/l8pr/static/components/NavPlayer/index.js | 38 ++++++-- app/l8pr/static/constants/index.js | 33 +++---- .../static/containers/Controller/index.js | 14 ++- app/l8pr/static/containers/Home/index.js | 26 +++--- app/l8pr/static/containers/Strip/index.js | 12 +-- .../static/containers/StripHeader/index.js | 4 +- app/l8pr/static/reducers/player.js | 62 +++++++++++-- app/l8pr/static/selectors.js | 35 +------- package.json | 2 +- 12 files changed, 316 insertions(+), 151 deletions(-) create mode 100644 .eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..2eaa40c --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,73 @@ +module.exports = { + env: { + browser: true, + commonjs: true, + es6: true, + }, + parser: 'babel-eslint', + extends: ['eslint:recommended'], + globals: { + $: false, + $$: false, + _: false, + angular: false, + browser: false, + by: false, + element: false, + gettext: false, + inject: false, + protractor: false, + requirejs: false + }, + parserOptions: { + ecmaFeatures: { + experimentalObjectRestSpread: true, + jsx: true + }, + sourceType: 'module' + }, + plugins: [ + 'react', + ], + rules: { + 'react/jsx-uses-react': 'error', + 'react/react-in-jsx-scope': 'error', + 'react/jsx-uses-vars': 'error', + 'react/jsx-no-undef': 'error', + 'react/prop-types': 2, + 'react/no-children-prop': 2, + 'react/no-did-update-set-state': 2, + 'react/no-direct-mutation-state': 2, + 'react/no-multi-comp': 2, + 'react/no-render-return-value': 2, + 'react/no-unknown-property': 2, + 'react/no-unused-prop-types': 2, + 'react/self-closing-comp': 2, + 'react/prefer-es6-class': 2, + 'no-trailing-spaces': 'error', + 'object-property-newline': 'error', + 'object-curly-newline': 'error', + 'comma-dangle': ['error', 'always-multiline'], + 'object-curly-spacing':['error', 'always'], + 'no-console': 'error', + 'block-scoped-var': 2, + complexity: [2, 10], + indent: [ + 'error', + 4, + { SwitchCase: 1 } + ], + 'linebreak-style': [ + 'error', + 'unix' + ], + quotes: [ + 'error', + 'single' + ], + semi: [ + 'error', + 'never' + ] + } +} diff --git a/app/l8pr/static/actions/data.js b/app/l8pr/static/actions/data.js index 5988b34..4fd0370 100644 --- a/app/l8pr/static/actions/data.js +++ b/app/l8pr/static/actions/data.js @@ -1,72 +1,80 @@ -import fetch from 'isomorphic-fetch'; -import { push } from 'react-router-redux'; +import fetch from 'isomorphic-fetch' +import { push } from 'react-router-redux' -import { SERVER_URL } from '../utils/config'; -import { checkHttpStatus, parseJSON } from '../utils'; -import { DATA_FETCH_PROTECTED_DATA_REQUEST, DATA_RECEIVE_PROTECTED_DATA, RECEIVE_LOOP } from '../constants'; -import { authLoginUserFailure } from './auth'; +import { SERVER_URL } from '../utils/config' +import { checkHttpStatus, parseJSON } from '../utils' +import * as c from '../constants' +import { authLoginUserFailure } from './auth' -export function fetchLastItems({user}) { - return (dispatch) => ( - fetch(`${SERVER_URL}/api/items/?users=${user}`, { - // credentials: 'include', - headers: { - Accept: 'application/json', - // Authorization: `Token ${token}` - } - }) - .then(checkHttpStatus) - .then(parseJSON) - ) +export function fetchLastItems({ user }) { + return fetch(`${SERVER_URL}/api/items/?users=${user}&limit=10`, { + // credentials: 'include', + headers: { + Accept: 'application/json', + // Authorization: `Token ${token}` + }, + }) + .then(checkHttpStatus) + .then(parseJSON) + .then((data) => data.results) +} + +export function fetchUserShows({ user }) { + return fetch(`${SERVER_URL}/api/loops/?user=${user}`, { + // credentials: 'include', + headers: { + Accept: 'application/json', + // Authorization: `Token ${token}` + }, + }) + .then(checkHttpStatus) + .then(parseJSON) + .then((items) => items[0].shows_list) } export function dataReceiveProtectedData(data) { return { - type: DATA_RECEIVE_PROTECTED_DATA, - payload: { - data - } - }; + type: c.DATA_RECEIVE_PROTECTED_DATA, + payload: { data }, + } } export function dataFetchProtectedDataRequest() { - return { - type: DATA_FETCH_PROTECTED_DATA_REQUEST - }; + return { type: c.DATA_FETCH_PROTECTED_DATA_REQUEST } } export function dataFetchProtectedData(token) { - return (dispatch, state) => { - dispatch(dataFetchProtectedDataRequest()); + return (dispatch) => { + dispatch(dataFetchProtectedDataRequest()) return fetch(`${SERVER_URL}/api/v1/getdata/`, { credentials: 'include', headers: { Accept: 'application/json', - Authorization: `Token ${token}` - } + Authorization: `Token ${token}`, + }, }) .then(checkHttpStatus) .then(parseJSON) .then((response) => { - dispatch(dataReceiveProtectedData(response.data)); + dispatch(dataReceiveProtectedData(response.data)) }) .catch((error) => { if (error && typeof error.response !== 'undefined' && error.response.status === 401) { // Invalid authentication credentials return error.response.json().then((data) => { - dispatch(authLoginUserFailure(401, data.non_field_errors[0])); - dispatch(push('/login')); - }); + dispatch(authLoginUserFailure(401, data.non_field_errors[0])) + dispatch(push('/login')) + }) } else if (error && typeof error.response !== 'undefined' && error.response.status >= 500) { // Server side error - dispatch(authLoginUserFailure(500, 'A server error occurred while sending your data!')); + dispatch(authLoginUserFailure(500, 'A server error occurred while sending your data!')) } else { // Most likely connection issues - dispatch(authLoginUserFailure('Connection Error', 'An error occurred while sending your data!')); + dispatch(authLoginUserFailure('Connection Error', 'An error occurred while sending your data!')) } - dispatch(push('/login')); - return Promise.resolve(); // TODO: we need a promise here because of the tests, find a better way - }); - }; + dispatch(push('/login')) + return Promise.resolve() // TODO: we need a promise here because of the tests, find a better way + }) + } } diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index 23d6472..39e67c9 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -1,9 +1,6 @@ -import fetch from 'isomorphic-fetch'; -import { SERVER_URL } from '../utils/config'; -import { checkHttpStatus, parseJSON } from '../utils'; -import * as c from '../constants'; +import { fetchLastItems, fetchUserShows } from './data' +import * as c from '../constants' import * as selectors from '../selectors' -import { get } from 'lodash' import { push } from 'react-router-redux' export function setPlaylist(playlist) { @@ -13,6 +10,44 @@ export function setPlaylist(playlist) { } } +export function initQueueList() { + return (dispatch) => ( + Promise.all([ + // LAST ITEMS + fetchLastItems({ user: 1 }) + // set context + .then((items) => (items.map((i) => ({ + ...i, + context: { title: 'Last Items' }, + })))), + // SHOWS + fetchUserShows({ user: 1 }) + .then((shows) => ( + shows.map((show) => ( + // set context + show.items.map((i) => ({ + ...i, + context: { + ...show, + items: null, + }, + })) + )) + )) + // flatten items + .then((showsItems) => ([].concat.apply([], showsItems))), + ]) + // flatten + .then((results) => ([].concat.apply([], results))) + // add to playlist + .then((items) => (dispatch(setPlaylist(items)))) + // set the current item + .then(() => dispatch(next())) + // start the player + .then(() => dispatch(play())) + ) +} + export function play(showAndItem) { return (dispatch, getState) => { dispatch({ @@ -31,31 +66,30 @@ export function play(showAndItem) { } } +export function previousContext() { + return { type: c.PREVIOUS_CONTEXT } +} + +export function nextContext() { + return { type: c.NEXT_CONTEXT } +} + export function next() { - return (dispatch, getState) => { - let nextItem = undefined - let nextShow = undefined - const currentShow = selectors.getCurrentShow(getState()) - const itemPositionInShow = selectors.getCurrentTrackPositionInShow(getState()) - if (itemPositionInShow < currentShow.items.length - 1) { - nextItem = currentShow.items[itemPositionInShow + 1] - } else { - const showPosition = selectors.getCurrentShowPositionInShow(getState()) - const playlist = selectors.playlist(getState()) - nextShow = playlist[(showPosition + 1) % playlist.length] - nextItem = nextShow.items[0] - } - return dispatch(play({ - item: get(nextItem, 'id'), - show: get(nextShow, 'id'), - })) - } + return { type: c.NEXT } } export function previous() { return { type: c.PREVIOUS } } +export function mute() { + return { type: c.MUTE } +} + +export function unmute() { + return { type: c.UNMUTE } +} + export function pause() { return { type: c.PAUSE } } diff --git a/app/l8pr/static/components/NavPlayer/index.js b/app/l8pr/static/components/NavPlayer/index.js index 1614929..cb61a2c 100644 --- a/app/l8pr/static/components/NavPlayer/index.js +++ b/app/l8pr/static/components/NavPlayer/index.js @@ -2,19 +2,21 @@ import React from 'react' import './style.scss' export default function NavPlayer({ - onPreviousShow, + onPreviousContext, onPreviousItem, onPlay, onPause, onNextItem, - onNextShow, + onNextContext, onMute, + onUnmute, + muted, playing, }) { return ( ) } + +NavPlayer.propTypes = { + onPreviousContext: React.PropTypes.func, + onPreviousItem: React.PropTypes.func, + onPlay: React.PropTypes.func, + onPause: React.PropTypes.func, + onNextItem: React.PropTypes.func, + onNextContext: React.PropTypes.func, + onMute: React.PropTypes.func, + onUnmute: React.PropTypes.func, + playing: React.PropTypes.bool, + muted: React.PropTypes.bool, +} diff --git a/app/l8pr/static/constants/index.js b/app/l8pr/static/constants/index.js index ddf63d5..599b47a 100644 --- a/app/l8pr/static/constants/index.js +++ b/app/l8pr/static/constants/index.js @@ -1,20 +1,23 @@ -export const AUTH_LOGIN_USER_REQUEST = 'AUTH_LOGIN_USER_REQUEST'; -export const AUTH_LOGIN_USER_FAILURE = 'AUTH_LOGIN_USER_FAILURE'; -export const AUTH_LOGIN_USER_SUCCESS = 'AUTH_LOGIN_USER_SUCCESS'; -export const AUTH_LOGOUT_USER = 'AUTH_LOGOUT_USER'; +export const AUTH_LOGIN_USER_REQUEST = 'AUTH_LOGIN_USER_REQUEST' +export const AUTH_LOGIN_USER_FAILURE = 'AUTH_LOGIN_USER_FAILURE' +export const AUTH_LOGIN_USER_SUCCESS = 'AUTH_LOGIN_USER_SUCCESS' +export const AUTH_LOGOUT_USER = 'AUTH_LOGOUT_USER' -export const DATA_FETCH_PROTECTED_DATA_REQUEST = 'DATA_FETCH_PROTECTED_DATA_REQUEST'; -export const DATA_RECEIVE_PROTECTED_DATA = 'DATA_RECEIVE_PROTECTED_DATA'; - -export const SET_LOOP = 'SET_LOOP'; -export const SET_SHOWS = 'SET_SHOWS'; -export const PLAY_ITEM = 'PLAY_ITEM'; -export const PAUSE = 'PAUSE'; -export const PLAY = 'PLAY'; -export const MUTE = 'MUTE'; -export const UNMUTE = 'UNMUTE'; -export const INSERT_SHOW = 'INSERT_SHOW'; +export const DATA_FETCH_PROTECTED_DATA_REQUEST = 'DATA_FETCH_PROTECTED_DATA_REQUEST' +export const DATA_RECEIVE_PROTECTED_DATA = 'DATA_RECEIVE_PROTECTED_DATA' +export const SET_LOOP = 'SET_LOOP' +export const SET_SHOWS = 'SET_SHOWS' +export const PLAY_ITEM = 'PLAY_ITEM' +export const PAUSE = 'PAUSE' +export const PLAY = 'PLAY' +export const NEXT = 'NEXT' +export const PREVIOUS = 'PREVIOUS' +export const MUTE = 'MUTE' +export const UNMUTE = 'UNMUTE' +export const INSERT_SHOW = 'INSERT_SHOW' +export const NEXT_CONTEXT = 'NEXT_CONTEXT' +export const PREVIOUS_CONTEXT = 'PREVIOUS_CONTEXT' export const RECEIVE_LOOP = 'RECEIVE_LOOP' export const SET_PLAYLIST = 'SET_PLAYLIST' diff --git a/app/l8pr/static/containers/Controller/index.js b/app/l8pr/static/containers/Controller/index.js index 4d5b3c4..9549903 100644 --- a/app/l8pr/static/containers/Controller/index.js +++ b/app/l8pr/static/containers/Controller/index.js @@ -1,7 +1,7 @@ import React from 'react' import { connect } from 'react-redux' import NavPlayer from '../../components/NavPlayer' -import { next, play, pause } from '../../actions/player' +import * as player from '../../actions/player' function ControllerComponent(props) { return ( @@ -11,11 +11,17 @@ function ControllerComponent(props) { const mapStateToProps = (state) => ({ playing: state.player.playing, + muted: state.player.muted, }) const mapDispatchToProps = (dispatch) => ({ - onNextItem: () => (dispatch(next())), - onPlay: () => (dispatch(play())), - onPause: () => (dispatch(pause())), + onPlay: () => (dispatch(player.play())), + onPause: () => (dispatch(player.pause())), + onNextItem: () => (dispatch(player.next())), + onNextContext: () => (dispatch(player.nextContext())), + onPreviousItem: () => (dispatch(player.previous())), + onPreviousContext: () => (dispatch(player.previousContext())), + onMute: () => (dispatch(player.mute())), + onUnmute: () => (dispatch(player.unmute())), }) export default connect(mapStateToProps, mapDispatchToProps)(ControllerComponent) diff --git a/app/l8pr/static/containers/Home/index.js b/app/l8pr/static/containers/Home/index.js index c029b91..5bac5a4 100644 --- a/app/l8pr/static/containers/Home/index.js +++ b/app/l8pr/static/containers/Home/index.js @@ -1,23 +1,25 @@ import React from 'react' -import { Link } from 'react-router' import { connect } from 'react-redux' import { Screen } from '../../components' import { Strip } from '../index' -import { fetchLastItems } from '../../actions/data' -import { setPlaylist, play, pause, next } from '../../actions/player' +import { play, pause, next, initQueueList } from '../../actions/player' import * as selectors from '../../selectors' import './style.scss' class HomeView extends React.Component { componentWillMount() { - this.props.fetchLastItems({ user: 1 }) - .then((items) => this.props.setPlaylist(items)) - .then(() => this.props.play(this.props.location)) + this.props.initQueueList() } static propTypes = { - media: React.PropTypes.object + media: React.PropTypes.object, + playing: React.PropTypes.bool, + nextItem: React.PropTypes.func, + play: React.PropTypes.func, + pause: React.PropTypes.func, + initQueueList: React.PropTypes.func, + volume: React.PropTypes.number, } render() { @@ -27,6 +29,7 @@ class HomeView extends React.Component { ({ - media: selectors.getCurrentTrack(state), - location: selectors.getLocation(state), - playing: state.player.playing, + media: selectors.currentTrack(state), + playing: state.player.playing, + volume: state.player.muted ? 0 : 1, }) const mapDispatchToProps = (dispatch) => ({ nextItem: () => (dispatch(next())), - fetchLastItems: (d) => (dispatch(fetchLastItems(d))), - setPlaylist: (loop) => (dispatch(setPlaylist(loop))), play: (args) => (dispatch(play(args))), pause: (args) => (dispatch(pause(args))), + initQueueList: () => (dispatch(initQueueList())), }) export default connect(mapStateToProps, mapDispatchToProps)(HomeView) diff --git a/app/l8pr/static/containers/Strip/index.js b/app/l8pr/static/containers/Strip/index.js index 1d7e198..32a9aff 100644 --- a/app/l8pr/static/containers/Strip/index.js +++ b/app/l8pr/static/containers/Strip/index.js @@ -1,12 +1,14 @@ -import React from 'react'; -import { connect } from 'react-redux'; +import React from 'react' +import { connect } from 'react-redux' import { Loop } from '../../components' -import { StripHeader, Controller} from '../index' +import { StripHeader, Controller } from '../index' import './style.scss' class Strip extends React.Component { static propTypes = { - }; + stripOpened: React.PropTypes.bool, + loopItems: React.PropTypes.array, + } render() { const { stripOpened, loopItems } = this.props @@ -24,6 +26,6 @@ class Strip extends React.Component { const mapStateToProps = (state) => ({ stripOpened: state.browser.stripOpened, - loopItems: state.browser.loop + loopItems: state.browser.loop, }) export default connect(mapStateToProps)(Strip) diff --git a/app/l8pr/static/containers/StripHeader/index.js b/app/l8pr/static/containers/StripHeader/index.js index 72952af..22b4908 100644 --- a/app/l8pr/static/containers/StripHeader/index.js +++ b/app/l8pr/static/containers/StripHeader/index.js @@ -24,8 +24,8 @@ function StripHeaderComponent({ trackTitle, stripOpened, toggleStrip, showTitle, } const mapStateToProps = (state) => ({ - trackTitle: get(selectors.getCurrentTrack(state), 'title'), - showTitle: get(selectors.getCurrentShow(state), 'title'), + trackTitle: get(selectors.currentTrack(state), 'title'), + showTitle: get(selectors.currentShow(state), 'title'), stripOpened: state.browser.stripOpened, }) diff --git a/app/l8pr/static/reducers/player.js b/app/l8pr/static/reducers/player.js index 097cdf2..7bb8fe0 100644 --- a/app/l8pr/static/reducers/player.js +++ b/app/l8pr/static/reducers/player.js @@ -2,16 +2,55 @@ import * as c from '../constants' import { get } from 'lodash' export default function (state = { - playlist: [], - currentTrack: 0, + current: null, + playQueue: [], + history: [], muted: false, - playing: false + playing: false, }, action = null) { switch (action.type) { + case c.NEXT: + var playQueue = [...state.playQueue] + var next = playQueue.shift() + return { + ...state, + current: next, + playQueue, + history: [state.current, ...state.history], + } + case c.PREVIOUS: + var history = [...state.history] + var previous = history.shift() + return { + ...state, + current: previous, + playQueue: [state.current, ...state.playQueue], + history, + } + case c.NEXT_CONTEXT: + var nextIndex = state.playQueue.findIndex( + (i) => (get(i, 'context.id') !== get(state.current, 'context.id')) + ) + return { + ...state, + current: state.playQueue[nextIndex], + playQueue: state.playQueue.slice(nextIndex + 1), + history: [...state.playQueue.slice(0, nextIndex).reverse(), ...state.history], + } + case c.PREVIOUS_CONTEXT: + var previousIndex = state.history.findIndex( + (i) => (get(i, 'context.id') !== get(state.current, 'context.id')) + ) + return { + ...state, + current: state.history[previousIndex], + history: state.history.slice(previousIndex + 1), + playQueue: [...state.history.slice(0, previousIndex).reverse(), ...state.playQueue], + } case c.SET_PLAYLIST: return { ...state, - playlist: action.payload, + playQueue: action.payload, } case c.PLAY: return { @@ -19,11 +58,20 @@ export default function (state = { playing: true, } case c.PAUSE: - return { ...state, playing: false } + return { + ...state, + playing: false, + } case c.MUTE: - return { ...state, muted: true } + return { + ...state, + muted: true, + } case c.UNMUTE: - return { ...state, muted: false } + return { + ...state, + muted: false, + } default: return state } diff --git a/app/l8pr/static/selectors.js b/app/l8pr/static/selectors.js index 64c5469..2a7604e 100644 --- a/app/l8pr/static/selectors.js +++ b/app/l8pr/static/selectors.js @@ -1,42 +1,11 @@ import { createSelector } from 'reselect' import { get } from 'lodash' -export const currentTrack = (state) => state.player.currentTrack -export const currentShow = (state) => state.player.currentShow +export const currentTrack = (state) => state.player.current +export const currentShow = (state) => get(state, 'player.current.context') export const playlist = (state) => state.player.playlist export const getPathname = (state) => state.routing.locationBeforeTransitions.pathname -export const getCurrentTrack = createSelector( - [playlist, currentTrack], - (playlist, currentTrackPosition) => { - return playlist[currentTrackPosition] - } -) - -export const getCurrentShow = createSelector( - [currentShow, playlist], - (currentShowId, playlist) => { - return playlist.find((s) => s.id === currentShowId) - } -) - - -export const getCurrentShowPositionInShow = createSelector( - [currentShow, playlist], - (currentShowId, playlist) => { - return playlist.findIndex((s) => s.id === currentShowId) - } -) - -export const getCurrentTrackPositionInShow = createSelector( - [getCurrentShow, currentTrack], - (currentShow, currentTrackId) => { - if (currentShow) { - return currentShow.items.findIndex((i) => i.id === currentTrackId) - } - } -) - export const getLocation = createSelector( [getPathname], (pathname) => { diff --git a/package.json b/package.json index bf4cac4..c5b0bbc 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "eslint-config-airbnb": "13.0.0", "eslint-plugin-import": "2.2.0", "eslint-plugin-jsx-a11y": "2.2.3", - "eslint-plugin-react": "6.8.0", + "eslint-plugin-react": "^6.8.0", "expect": "1.20.2", "ignore-styles": "5.0.01", "istanbul": "1.0.0-alpha.2", From 5c96cac49ece74f83ab1da01f5dadd7b94980afa Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Tue, 11 Apr 2017 18:02:42 +0200 Subject: [PATCH 005/143] login --- .bootstraprc | 4 +- app/l8pr/static/actions/auth.js | 99 +- app/l8pr/static/actions/modal.js | 6 + .../static/components/LoginModal/index.js | 31 + app/l8pr/static/components/index.js | 1 + app/l8pr/static/containers/Home/index.js | 3 +- app/l8pr/static/containers/Login/index.js | 105 +-- .../static/containers/ModalsContainer.jsx | 33 + .../static/containers/StripHeader/index.js | 3 +- app/l8pr/static/containers/index.js | 11 +- app/l8pr/static/reducers/auth.js | 5 +- app/l8pr/static/reducers/index.js | 16 +- app/l8pr/static/reducers/modal.js | 22 + app/l8pr/static/styles/config/_colors.scss | 2 +- app/l8pr/static/styles/config/_variables.scss | 875 ++++++++++++++++++ app/l8pr/static/styles/theme/_login.scss | 7 - app/settings.py | 10 +- app/urls.py | 9 +- package.json | 1 + requirements.txt | 5 +- 20 files changed, 1107 insertions(+), 141 deletions(-) create mode 100644 app/l8pr/static/actions/modal.js create mode 100644 app/l8pr/static/components/LoginModal/index.js create mode 100644 app/l8pr/static/containers/ModalsContainer.jsx create mode 100644 app/l8pr/static/reducers/modal.js diff --git a/.bootstraprc b/.bootstraprc index 1854a93..195aedb 100644 --- a/.bootstraprc +++ b/.bootstraprc @@ -39,7 +39,7 @@ extractStyles: true # Thus, you may customize Bootstrap variables # based on the values established in the Bootstrap _variables.scss file # -# bootstrapCustomizations: ./path/to/bootstrap/customizations.scss +bootstrapCustomizations: ./app/l8pr/static/styles/config/_variables.scss # Import your custom styles here @@ -114,4 +114,4 @@ scripts: popover: true scrollspy: true tab: true - affix: true \ No newline at end of file + affix: true diff --git a/app/l8pr/static/actions/auth.js b/app/l8pr/static/actions/auth.js index de2141f..30ef9c9 100644 --- a/app/l8pr/static/actions/auth.js +++ b/app/l8pr/static/actions/auth.js @@ -1,92 +1,97 @@ -import fetch from 'isomorphic-fetch'; -import { push } from 'react-router-redux'; -import { SERVER_URL } from '../utils/config'; -import { checkHttpStatus, parseJSON } from '../utils'; +import fetch from 'isomorphic-fetch' +import { push } from 'react-router-redux' +import { hideModal } from '../actions/modal' +import { SERVER_URL } from '../utils/config' +import { checkHttpStatus, parseJSON } from '../utils' import { AUTH_LOGIN_USER_REQUEST, AUTH_LOGIN_USER_FAILURE, AUTH_LOGIN_USER_SUCCESS, - AUTH_LOGOUT_USER -} from '../constants'; + AUTH_LOGOUT_USER, +} from '../constants' export function authLoginUserSuccess(token, user) { - sessionStorage.setItem('token', token); - sessionStorage.setItem('user', JSON.stringify(user)); + sessionStorage.setItem('token', token) + sessionStorage.setItem('user', JSON.stringify(user)) return { type: AUTH_LOGIN_USER_SUCCESS, payload: { token, - user - } - }; + user, + }, + } } export function authLoginUserFailure(error, message) { - sessionStorage.removeItem('token'); + sessionStorage.removeItem('token') return { type: AUTH_LOGIN_USER_FAILURE, payload: { status: error, - statusText: message - } - }; + statusText: message, + }, + } } export function authLoginUserRequest() { - return { - type: AUTH_LOGIN_USER_REQUEST - } + return { type: AUTH_LOGIN_USER_REQUEST } } export function authLogout() { - sessionStorage.removeItem('token'); - sessionStorage.removeItem('user'); - return { - type: AUTH_LOGOUT_USER - }; + sessionStorage.removeItem('token') + sessionStorage.removeItem('user') + return { type: AUTH_LOGOUT_USER } } export function authLogoutAndRedirect() { - return (dispatch, state) => { - dispatch(authLogout()); - dispatch(push('/login')); - return Promise.resolve(); // TODO: we need a promise here because of the tests, find a better way - }; + return (dispatch) => { + dispatch(authLogout()) + dispatch(push('/login')) + return Promise.resolve() // TODO: we need a promise here because of the tests, find a better way + } } export function authLoginUser(email, password, redirect = '/') { return (dispatch) => { - dispatch(authLoginUserRequest()); - const auth = btoa(`${email}:${password}`); - return fetch(`${SERVER_URL}/api/v1/accounts/login/`, { + dispatch(authLoginUserRequest()) + var formData = new FormData() + formData.append('password', password) + formData.append('username', email) + return fetch(`${SERVER_URL}/auth/login/`, { method: 'post', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Basic ${auth}` - } + body: formData, + headers: { Accept: 'application/json' }, }) .then(checkHttpStatus) .then(parseJSON) - .then((response) => { - dispatch(authLoginUserSuccess(response.token, response.user)); - dispatch(push(redirect)); - }) + .then((response) => ( + fetch(`${SERVER_URL}/auth/me/`, { + method: 'get', + headers: { Authorization: `Token ${response.auth_token}` }, + }) + .then(checkHttpStatus) + .then(parseJSON) + .then((user) => { + dispatch(authLoginUserSuccess(response.auth_token, user)) + dispatch(hideModal()) + dispatch(push(redirect)) + }) + )) .catch((error) => { if (error && typeof error.response !== 'undefined' && error.response.status === 401) { // Invalid authentication credentials return error.response.json().then((data) => { - dispatch(authLoginUserFailure(401, data.non_field_errors[0])); - }); + dispatch(authLoginUserFailure(401, data.non_field_errors[0])) + }) } else if (error && typeof error.response !== 'undefined' && error.response.status >= 500) { // Server side error - dispatch(authLoginUserFailure(500, 'A server error occurred while sending your data!')); + dispatch(authLoginUserFailure(500, 'A server error occurred while sending your data!')) } else { // Most likely connection issues - dispatch(authLoginUserFailure('Connection Error', 'An error occurred while sending your data!')); + dispatch(authLoginUserFailure('Connection Error', 'An error occurred while sending your data!')) } - return Promise.resolve(); // TODO: we need a promise here because of the tests, find a better way - }); - }; + return Promise.resolve() // TODO: we need a promise here because of the tests, find a better way + }) + } } diff --git a/app/l8pr/static/actions/modal.js b/app/l8pr/static/actions/modal.js new file mode 100644 index 0000000..1745e40 --- /dev/null +++ b/app/l8pr/static/actions/modal.js @@ -0,0 +1,6 @@ +export const showModal = ({ modalType, modalProps=undefined }) => ({ + type: 'SHOW_MODAL', + modalType, + modalProps, +}) +export const hideModal = () => ({ type: 'HIDE_MODAL' }) diff --git a/app/l8pr/static/components/LoginModal/index.js b/app/l8pr/static/components/LoginModal/index.js new file mode 100644 index 0000000..aa390a3 --- /dev/null +++ b/app/l8pr/static/components/LoginModal/index.js @@ -0,0 +1,31 @@ +import React from 'react' +import { Modal, Button } from 'react-bootstrap' +import { LoginView } from '../../containers' + +function LoginModal({ handleHide }) { + var form = null + function login() { + form && form.login() + } + return ( + + + + + +

Login

+
+ + {if (r) form = r.getWrappedInstance()}}/> + + + + + +
+ ) +} + +LoginModal.propTypes = { handleHide: React.PropTypes.func.isRequired } + +export default LoginModal diff --git a/app/l8pr/static/components/index.js b/app/l8pr/static/components/index.js index dd2f51b..d234979 100644 --- a/app/l8pr/static/components/index.js +++ b/app/l8pr/static/components/index.js @@ -1,3 +1,4 @@ export Screen from './Screen' export Loop from './Loop' export LoopItem from './LoopItem' +export LoginModal from './LoginModal' diff --git a/app/l8pr/static/containers/Home/index.js b/app/l8pr/static/containers/Home/index.js index 5bac5a4..4ab9c1c 100644 --- a/app/l8pr/static/containers/Home/index.js +++ b/app/l8pr/static/containers/Home/index.js @@ -1,7 +1,7 @@ import React from 'react' import { connect } from 'react-redux' import { Screen } from '../../components' -import { Strip } from '../index' +import { Strip, ModalsContainer } from '../index' import { play, pause, next, initQueueList } from '../../actions/player' import * as selectors from '../../selectors' import './style.scss' @@ -25,6 +25,7 @@ class HomeView extends React.Component { render() { return (
+ {this.props.media && Hint: a@a.com / qw, - fields: { - password: { - type: 'password' - } - } -}; + fields: { password: { type: 'password' } }, +} class LoginView extends React.Component { @@ -31,53 +26,49 @@ class LoginView extends React.Component { isAuthenticated: React.PropTypes.bool.isRequired, isAuthenticating: React.PropTypes.bool.isRequired, statusText: React.PropTypes.string, - actions: React.PropTypes.shape({ - authLoginUser: React.PropTypes.func.isRequired - }).isRequired, - location: React.PropTypes.shape({ - query: React.PropTypes.object.isRequired - }) - }; + actions: React.PropTypes.shape({ authLoginUser: React.PropTypes.func.isRequired }).isRequired, + location: React.PropTypes.shape({ query: React.PropTypes.object.isRequired }), + } constructor(props) { - super(props); + super(props) - const redirectRoute = this.props.location ? this.props.location.query.next || '/' : '/'; + const redirectRoute = this.props.location ? this.props.location.query.next || '/' : '/' this.state = { formValues: { - email: '', - password: '' + username: '', + password: '', }, - redirectTo: redirectRoute - }; + redirectTo: redirectRoute, + } } componentWillMount() { if (this.props.isAuthenticated) { - this.props.dispatch(push('/')); + this.props.dispatch(push('/')) } } onFormChange = (value) => { - this.setState({ formValues: value }); - }; + this.setState({ formValues: value }) + } login = (e) => { - e.preventDefault(); - const value = this.loginForm.getValue(); + if (e) e.preventDefault() + const value = this.loginForm.getValue() if (value) { - this.props.actions.authLoginUser(value.email, value.password, this.state.redirectTo); + this.props.actions.authLoginUser(value.username, value.password, this.state.redirectTo) } - }; + } render() { - let statusText = null; + let statusText = null if (this.props.statusText) { const statusTextClassNames = classNames({ 'alert': true, 'alert-danger': this.props.statusText.indexOf('Authentication Error') === 0, - 'alert-success': this.props.statusText.indexOf('Authentication Error') !== 0 - }); + 'alert-success': this.props.statusText.indexOf('Authentication Error') !== 0, + }) statusText = (
@@ -87,16 +78,15 @@ class LoginView extends React.Component {
- ); + ) } return ( -
-

Login

+
{statusText}
- { this.loginForm = ref; }} + { this.loginForm = ref }} type={Login} options={LoginFormOptions} value={this.state.formValues} @@ -104,14 +94,14 @@ class LoginView extends React.Component { />
- ); + ) } } @@ -119,16 +109,21 @@ const mapStateToProps = (state) => { return { isAuthenticated: state.auth.isAuthenticated, isAuthenticating: state.auth.isAuthenticating, - statusText: state.auth.statusText - }; -}; + statusText: state.auth.statusText, + } +} const mapDispatchToProps = (dispatch) => { return { dispatch, - actions: bindActionCreators(actionCreators, dispatch) - }; -}; + actions: bindActionCreators(actionCreators, dispatch), + } +} -export default connect(mapStateToProps, mapDispatchToProps)(LoginView); -export { LoginView as LoginViewNotConnected }; +export default connect( + mapStateToProps, + mapDispatchToProps, + null, + { withRef: true }, +)(LoginView) +export { LoginView as LoginViewNotConnected } diff --git a/app/l8pr/static/containers/ModalsContainer.jsx b/app/l8pr/static/containers/ModalsContainer.jsx new file mode 100644 index 0000000..ce74b6b --- /dev/null +++ b/app/l8pr/static/containers/ModalsContainer.jsx @@ -0,0 +1,33 @@ +import React from 'react' +import { connect } from 'react-redux' +import { hideModal } from '../actions/modal' +import { LoginModal } from '../components' + +const modals = { + LOGIN: LoginModal, +} + +export function Modals({ modalType, modalProps, handleHide }) { + if (modalType) { + return React.createElement(modals[modalType], { + handleHide, + modalProps, + }) + } else { + return null + } +} + + +Modals.propTypes = { + modalType: React.PropTypes.string, + modalProps: React.PropTypes.object, + handleHide: React.PropTypes.func.isRequired, +} + +const mapStateToProps = (state) => ({ + modalType: state.modal.modalType, + modalProps: state.modal.modalProps, +}) +const mapDispatchToProps = (dispatch) => ({ handleHide: () => dispatch(hideModal()) }) +export const ModalsContainer = connect(mapStateToProps, mapDispatchToProps)(Modals) diff --git a/app/l8pr/static/containers/StripHeader/index.js b/app/l8pr/static/containers/StripHeader/index.js index 22b4908..d801a45 100644 --- a/app/l8pr/static/containers/StripHeader/index.js +++ b/app/l8pr/static/containers/StripHeader/index.js @@ -2,6 +2,7 @@ import React from 'react' import { connect } from 'react-redux' import * as selectors from '../../selectors' import { toggleStrip } from '../../actions/browser' +import { showModal } from '../../actions/modal' import { get } from 'lodash' import './style.scss' @@ -31,6 +32,6 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => ({ toggleStrip: () => (dispatch(toggleStrip())), - onLogin: () => (dispatch(login())), + onLogin: () => (dispatch(showModal({ modalType: 'LOGIN' }))), }) export default connect(mapStateToProps, mapDispatchToProps)(StripHeaderComponent) diff --git a/app/l8pr/static/containers/index.js b/app/l8pr/static/containers/index.js index fa63771..745a20d 100644 --- a/app/l8pr/static/containers/index.js +++ b/app/l8pr/static/containers/index.js @@ -1,7 +1,8 @@ -export HomeView from './Home/index'; -export LoginView from './Login/index'; -export ProtectedView from './Protected/index'; -export NotFoundView from './NotFound/index'; -export Strip from './Strip/index'; +export HomeView from './Home/index' +export LoginView from './Login/index' +export ProtectedView from './Protected/index' +export NotFoundView from './NotFound/index' +export Strip from './Strip/index' export StripHeader from './StripHeader/index' export Controller from './Controller/index' +export { ModalsContainer } from './ModalsContainer' diff --git a/app/l8pr/static/reducers/auth.js b/app/l8pr/static/reducers/auth.js index 237381f..b20f0b4 100644 --- a/app/l8pr/static/reducers/auth.js +++ b/app/l8pr/static/reducers/auth.js @@ -9,7 +9,6 @@ import { const initialState = { token: null, - userName: null, isAuthenticated: false, isAuthenticating: false, statusText: null @@ -27,7 +26,7 @@ export default createReducer(initialState, { isAuthenticating: false, isAuthenticated: true, token: payload.token, - userName: payload.user.email, + user: payload.user, statusText: 'You have been successfully logged in.' }); }, @@ -36,7 +35,6 @@ export default createReducer(initialState, { isAuthenticating: false, isAuthenticated: false, token: null, - userName: null, statusText: `Authentication Error: ${payload.status} - ${payload.statusText}` }); }, @@ -44,7 +42,6 @@ export default createReducer(initialState, { return Object.assign({}, state, { isAuthenticated: false, token: null, - userName: null, statusText: 'You have been successfully logged out.' }); } diff --git a/app/l8pr/static/reducers/index.js b/app/l8pr/static/reducers/index.js index ab71aa8..57aa6df 100644 --- a/app/l8pr/static/reducers/index.js +++ b/app/l8pr/static/reducers/index.js @@ -1,9 +1,10 @@ -import { combineReducers } from 'redux'; -import { routerReducer } from 'react-router-redux'; -import authReducer from './auth'; -import dataReducer from './data'; -import playerReducer from './player'; -import browserReducer from './browser'; +import { combineReducers } from 'redux' +import { routerReducer } from 'react-router-redux' +import authReducer from './auth' +import dataReducer from './data' +import playerReducer from './player' +import browserReducer from './browser' +import modalReducer from './modal' export default combineReducers({ auth: authReducer, @@ -11,4 +12,5 @@ export default combineReducers({ routing: routerReducer, player: playerReducer, browser: browserReducer, -}); + modal: modalReducer, +}) diff --git a/app/l8pr/static/reducers/modal.js b/app/l8pr/static/reducers/modal.js new file mode 100644 index 0000000..28be44b --- /dev/null +++ b/app/l8pr/static/reducers/modal.js @@ -0,0 +1,22 @@ +const initialState = { + modalType: null, + modalProps: undefined, + previousState: undefined, +} + +const modal = (state = initialState, action) => { + switch (action.type) { + case 'SHOW_MODAL': + return { + modalType: action.modalType, + modalProps: action.modalProps, + previousState: state, + } + case 'HIDE_MODAL': + return state.previousState || initialState + default: + return state + } +} + +export default modal diff --git a/app/l8pr/static/styles/config/_colors.scss b/app/l8pr/static/styles/config/_colors.scss index ef583b5..2547fa8 100644 --- a/app/l8pr/static/styles/config/_colors.scss +++ b/app/l8pr/static/styles/config/_colors.scss @@ -13,4 +13,4 @@ $color-green-default: #16967a; $color-green: #89d78f; $color-green-light: #b3ffd8; -$color-blue: #35A1FB; \ No newline at end of file +$color-blue: #35A1FB; diff --git a/app/l8pr/static/styles/config/_variables.scss b/app/l8pr/static/styles/config/_variables.scss index 5d562ad..688f25f 100644 --- a/app/l8pr/static/styles/config/_variables.scss +++ b/app/l8pr/static/styles/config/_variables.scss @@ -1,3 +1,878 @@ @import "colors"; $font-family-regular: Arial; + +$bootstrap-sass-asset-helper: false !default; +// +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +$gray-base: #000; +$gray-darker: lighten($gray-base, 13.5%); // #222 +$gray-dark: lighten($gray-base, 20%); // #333 +$gray: lighten($gray-base, 33.5%); // #555 +$gray-light: lighten($gray-base, 46.7%); // #777 +$gray-lighter: lighten($gray-base, 93.5%); // #eee + +$brand-primary: purple; // #337ab7 +$brand-success: #5cb85c; +$brand-info: #5bc0de; +$brand-warning: #f0ad4e; +$brand-danger: #d9534f; + + +//== Scaffolding +// +//## Settings for some of the most global styles. + +//** Background color for ``. +$body-bg: #fff !default; +//** Global text color on ``. +$text-color: $gray-dark !default; + +//** Global textual link color. +$link-color: $brand-primary !default; +//** Link hover color set via `darken()` function. +$link-hover-color: darken($link-color, 15%) !default; +//** Link hover decoration. +$link-hover-decoration: underline !default; + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +$font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif !default; +$font-family-serif: Georgia, "Times New Roman", Times, serif !default; +//** Default monospace fonts for ``, ``, and `
`.
+$font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace !default;
+$font-family-base:        $font-family-sans-serif !default;
+
+$font-size-base:          14px !default;
+$font-size-large:         ceil(($font-size-base * 1.25)) !default; // ~18px
+$font-size-small:         ceil(($font-size-base * 0.85)) !default; // ~12px
+
+$font-size-h1:            floor(($font-size-base * 2.6)) !default; // ~36px
+$font-size-h2:            floor(($font-size-base * 2.15)) !default; // ~30px
+$font-size-h3:            ceil(($font-size-base * 1.7)) !default; // ~24px
+$font-size-h4:            ceil(($font-size-base * 1.25)) !default; // ~18px
+$font-size-h5:            $font-size-base !default;
+$font-size-h6:            ceil(($font-size-base * 0.85)) !default; // ~12px
+
+//** Unit-less `line-height` for use in components like buttons.
+$line-height-base:        1.428571429 !default; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+$line-height-computed:    floor(($font-size-base * $line-height-base)) !default; // ~20px
+
+//** By default, this inherits from the ``.
+$headings-font-family:    inherit !default;
+$headings-font-weight:    500 !default;
+$headings-line-height:    1.1 !default;
+$headings-color:          inherit !default;
+
+
+//== Iconography
+//
+//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+//** Load fonts from this directory.
+
+// [converter] If $bootstrap-sass-asset-helper if used, provide path relative to the assets load path.
+// [converter] This is because some asset helpers, such as Sprockets, do not work with file-relative paths.
+$icon-font-path: if($bootstrap-sass-asset-helper, "bootstrap/", "../fonts/bootstrap/") !default;
+
+//** File name for all font files.
+$icon-font-name:          "glyphicons-halflings-regular" !default;
+//** Element ID within SVG icon file.
+$icon-font-svg-id:        "glyphicons_halflingsregular" !default;
+
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+$padding-base-vertical:     6px !default;
+$padding-base-horizontal:   12px !default;
+
+$padding-large-vertical:    10px !default;
+$padding-large-horizontal:  16px !default;
+
+$padding-small-vertical:    5px !default;
+$padding-small-horizontal:  10px !default;
+
+$padding-xs-vertical:       1px !default;
+$padding-xs-horizontal:     5px !default;
+
+$line-height-large:         1.3333333 !default; // extra decimals for Win 8.1 Chrome
+$line-height-small:         1.5 !default;
+
+$border-radius-base:        4px !default;
+$border-radius-large:       6px !default;
+$border-radius-small:       3px !default;
+
+//** Global color for active items (e.g., navs or dropdowns).
+$component-active-color:    #fff !default;
+//** Global background color for active items (e.g., navs or dropdowns).
+$component-active-bg:       $brand-primary !default;
+
+//** Width of the `border` for generating carets that indicate dropdowns.
+$caret-width-base:          4px !default;
+//** Carets increase slightly in size for larger components.
+$caret-width-large:         5px !default;
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for ``s and ``s.
+$table-cell-padding:            8px !default;
+//** Padding for cells in `.table-condensed`.
+$table-condensed-cell-padding:  5px !default;
+
+//** Default background color used for all tables.
+$table-bg:                      transparent !default;
+//** Background color used for `.table-striped`.
+$table-bg-accent:               #f9f9f9 !default;
+//** Background color used for `.table-hover`.
+$table-bg-hover:                #f5f5f5 !default;
+$table-bg-active:               $table-bg-hover !default;
+
+//** Border color for table and cell borders.
+$table-border-color:            #ddd !default;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+$btn-font-weight:                normal !default;
+
+$btn-default-color:              #333 !default;
+$btn-default-bg:                 #fff !default;
+$btn-default-border:             #ccc !default;
+
+$btn-primary-color:              #fff !default;
+$btn-primary-bg:                 $brand-primary !default;
+$btn-primary-border:             darken($btn-primary-bg, 5%) !default;
+
+$btn-success-color:              #fff !default;
+$btn-success-bg:                 $brand-success !default;
+$btn-success-border:             darken($btn-success-bg, 5%) !default;
+
+$btn-info-color:                 #fff !default;
+$btn-info-bg:                    $brand-info !default;
+$btn-info-border:                darken($btn-info-bg, 5%) !default;
+
+$btn-warning-color:              #fff !default;
+$btn-warning-bg:                 $brand-warning !default;
+$btn-warning-border:             darken($btn-warning-bg, 5%) !default;
+
+$btn-danger-color:               #fff !default;
+$btn-danger-bg:                  $brand-danger !default;
+$btn-danger-border:              darken($btn-danger-bg, 5%) !default;
+
+$btn-link-disabled-color:        $gray-light !default;
+
+// Allows for customizing button radius independently from global border radius
+$btn-border-radius-base:         $border-radius-base !default;
+$btn-border-radius-large:        $border-radius-large !default;
+$btn-border-radius-small:        $border-radius-small !default;
+
+
+//== Forms
+//
+//##
+
+//** `` background color
+$input-bg:                       #fff !default;
+//** `` background color
+$input-bg-disabled:              $gray-lighter !default;
+
+//** Text color for ``s
+$input-color:                    $gray !default;
+//** `` border color
+$input-border:                   #ccc !default;
+
+// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
+//** Default `.form-control` border radius
+// This has no effect on ``s in CSS.
+$input-border-radius:            $border-radius-base !default;
+//** Large `.form-control` border radius
+$input-border-radius-large:      $border-radius-large !default;
+//** Small `.form-control` border radius
+$input-border-radius-small:      $border-radius-small !default;
+
+//** Border color for inputs on focus
+$input-border-focus:             #66afe9 !default;
+
+//** Placeholder text color
+$input-color-placeholder:        #999 !default;
+
+//** Default `.form-control` height
+$input-height-base:              ($line-height-computed + ($padding-base-vertical * 2) + 2) !default;
+//** Large `.form-control` height
+$input-height-large:             (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) !default;
+//** Small `.form-control` height
+$input-height-small:             (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) !default;
+
+//** `.form-group` margin
+$form-group-margin-bottom:       15px !default;
+
+$legend-color:                   $gray-dark !default;
+$legend-border-color:            #e5e5e5 !default;
+
+//** Background color for textual input addons
+$input-group-addon-bg:           $gray-lighter !default;
+//** Border color for textual input addons
+$input-group-addon-border-color: $input-border !default;
+
+//** Disabled cursor for form controls and buttons.
+$cursor-disabled:                not-allowed !default;
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+$dropdown-bg:                    #fff !default;
+//** Dropdown menu `border-color`.
+$dropdown-border:                rgba(0,0,0,.15) !default;
+//** Dropdown menu `border-color` **for IE8**.
+$dropdown-fallback-border:       #ccc !default;
+//** Divider color for between dropdown items.
+$dropdown-divider-bg:            #e5e5e5 !default;
+
+//** Dropdown link text color.
+$dropdown-link-color:            $gray-dark !default;
+//** Hover color for dropdown links.
+$dropdown-link-hover-color:      darken($gray-dark, 5%) !default;
+//** Hover background for dropdown links.
+$dropdown-link-hover-bg:         #f5f5f5 !default;
+
+//** Active dropdown menu item text color.
+$dropdown-link-active-color:     $component-active-color !default;
+//** Active dropdown menu item background color.
+$dropdown-link-active-bg:        $component-active-bg !default;
+
+//** Disabled dropdown menu item background color.
+$dropdown-link-disabled-color:   $gray-light !default;
+
+//** Text color for headers within dropdown menus.
+$dropdown-header-color:          $gray-light !default;
+
+//** Deprecated `$dropdown-caret-color` as of v3.1.0
+$dropdown-caret-color:           #000 !default;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+$zindex-navbar:            1000 !default;
+$zindex-dropdown:          1000 !default;
+$zindex-popover:           1060 !default;
+$zindex-tooltip:           1070 !default;
+$zindex-navbar-fixed:      1030 !default;
+$zindex-modal-background:  1040 !default;
+$zindex-modal:             1050 !default;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+//** Deprecated `$screen-xs` as of v3.0.1
+$screen-xs:                  480px !default;
+//** Deprecated `$screen-xs-min` as of v3.2.0
+$screen-xs-min:              $screen-xs !default;
+//** Deprecated `$screen-phone` as of v3.0.1
+$screen-phone:               $screen-xs-min !default;
+
+// Small screen / tablet
+//** Deprecated `$screen-sm` as of v3.0.1
+$screen-sm:                  768px !default;
+$screen-sm-min:              $screen-sm !default;
+//** Deprecated `$screen-tablet` as of v3.0.1
+$screen-tablet:              $screen-sm-min !default;
+
+// Medium screen / desktop
+//** Deprecated `$screen-md` as of v3.0.1
+$screen-md:                  992px !default;
+$screen-md-min:              $screen-md !default;
+//** Deprecated `$screen-desktop` as of v3.0.1
+$screen-desktop:             $screen-md-min !default;
+
+// Large screen / wide desktop
+//** Deprecated `$screen-lg` as of v3.0.1
+$screen-lg:                  1200px !default;
+$screen-lg-min:              $screen-lg !default;
+//** Deprecated `$screen-lg-desktop` as of v3.0.1
+$screen-lg-desktop:          $screen-lg-min !default;
+
+// So media queries don't overlap when required, provide a maximum
+$screen-xs-max:              ($screen-sm-min - 1) !default;
+$screen-sm-max:              ($screen-md-min - 1) !default;
+$screen-md-max:              ($screen-lg-min - 1) !default;
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+$grid-columns:              12 !default;
+//** Padding between columns. Gets divided in half for the left and right.
+$grid-gutter-width:         30px !default;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+$grid-float-breakpoint:     $screen-sm-min !default;
+//** Point at which the navbar begins collapsing.
+$grid-float-breakpoint-max: ($grid-float-breakpoint - 1) !default;
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+$container-tablet:             (720px + $grid-gutter-width) !default;
+//** For `$screen-sm-min` and up.
+$container-sm:                 $container-tablet !default;
+
+// Medium screen / desktop
+$container-desktop:            (940px + $grid-gutter-width) !default;
+//** For `$screen-md-min` and up.
+$container-md:                 $container-desktop !default;
+
+// Large screen / wide desktop
+$container-large-desktop:      (1140px + $grid-gutter-width) !default;
+//** For `$screen-lg-min` and up.
+$container-lg:                 $container-large-desktop !default;
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+$navbar-height:                    50px !default;
+$navbar-margin-bottom:             $line-height-computed !default;
+$navbar-border-radius:             $border-radius-base !default;
+$navbar-padding-horizontal:        floor(($grid-gutter-width / 2)) !default;
+$navbar-padding-vertical:          (($navbar-height - $line-height-computed) / 2) !default;
+$navbar-collapse-max-height:       340px !default;
+
+$navbar-default-color:             #777 !default;
+$navbar-default-bg:                #f8f8f8 !default;
+$navbar-default-border:            darken($navbar-default-bg, 6.5%) !default;
+
+// Navbar links
+$navbar-default-link-color:                #777 !default;
+$navbar-default-link-hover-color:          #333 !default;
+$navbar-default-link-hover-bg:             transparent !default;
+$navbar-default-link-active-color:         #555 !default;
+$navbar-default-link-active-bg:            darken($navbar-default-bg, 6.5%) !default;
+$navbar-default-link-disabled-color:       #ccc !default;
+$navbar-default-link-disabled-bg:          transparent !default;
+
+// Navbar brand label
+$navbar-default-brand-color:               $navbar-default-link-color !default;
+$navbar-default-brand-hover-color:         darken($navbar-default-brand-color, 10%) !default;
+$navbar-default-brand-hover-bg:            transparent !default;
+
+// Navbar toggle
+$navbar-default-toggle-hover-bg:           #ddd !default;
+$navbar-default-toggle-icon-bar-bg:        #888 !default;
+$navbar-default-toggle-border-color:       #ddd !default;
+
+
+//=== Inverted navbar
+// Reset inverted navbar basics
+$navbar-inverse-color:                      lighten($gray-light, 15%) !default;
+$navbar-inverse-bg:                         #222 !default;
+$navbar-inverse-border:                     darken($navbar-inverse-bg, 10%) !default;
+
+// Inverted navbar links
+$navbar-inverse-link-color:                 lighten($gray-light, 15%) !default;
+$navbar-inverse-link-hover-color:           #fff !default;
+$navbar-inverse-link-hover-bg:              transparent !default;
+$navbar-inverse-link-active-color:          $navbar-inverse-link-hover-color !default;
+$navbar-inverse-link-active-bg:             darken($navbar-inverse-bg, 10%) !default;
+$navbar-inverse-link-disabled-color:        #444 !default;
+$navbar-inverse-link-disabled-bg:           transparent !default;
+
+// Inverted navbar brand label
+$navbar-inverse-brand-color:                $navbar-inverse-link-color !default;
+$navbar-inverse-brand-hover-color:          #fff !default;
+$navbar-inverse-brand-hover-bg:             transparent !default;
+
+// Inverted navbar toggle
+$navbar-inverse-toggle-hover-bg:            #333 !default;
+$navbar-inverse-toggle-icon-bar-bg:         #fff !default;
+$navbar-inverse-toggle-border-color:        #333 !default;
+
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+$nav-link-padding:                          10px 15px !default;
+$nav-link-hover-bg:                         $gray-lighter !default;
+
+$nav-disabled-link-color:                   $gray-light !default;
+$nav-disabled-link-hover-color:             $gray-light !default;
+
+//== Tabs
+$nav-tabs-border-color:                     #ddd !default;
+
+$nav-tabs-link-hover-border-color:          $gray-lighter !default;
+
+$nav-tabs-active-link-hover-bg:             $body-bg !default;
+$nav-tabs-active-link-hover-color:          $gray !default;
+$nav-tabs-active-link-hover-border-color:   #ddd !default;
+
+$nav-tabs-justified-link-border-color:            #ddd !default;
+$nav-tabs-justified-active-link-border-color:     $body-bg !default;
+
+//== Pills
+$nav-pills-border-radius:                   $border-radius-base !default;
+$nav-pills-active-link-hover-bg:            $component-active-bg !default;
+$nav-pills-active-link-hover-color:         $component-active-color !default;
+
+
+//== Pagination
+//
+//##
+
+$pagination-color:                     $link-color !default;
+$pagination-bg:                        #fff !default;
+$pagination-border:                    #ddd !default;
+
+$pagination-hover-color:               $link-hover-color !default;
+$pagination-hover-bg:                  $gray-lighter !default;
+$pagination-hover-border:              #ddd !default;
+
+$pagination-active-color:              #fff !default;
+$pagination-active-bg:                 $brand-primary !default;
+$pagination-active-border:             $brand-primary !default;
+
+$pagination-disabled-color:            $gray-light !default;
+$pagination-disabled-bg:               #fff !default;
+$pagination-disabled-border:           #ddd !default;
+
+
+//== Pager
+//
+//##
+
+$pager-bg:                             $pagination-bg !default;
+$pager-border:                         $pagination-border !default;
+$pager-border-radius:                  15px !default;
+
+$pager-hover-bg:                       $pagination-hover-bg !default;
+
+$pager-active-bg:                      $pagination-active-bg !default;
+$pager-active-color:                   $pagination-active-color !default;
+
+$pager-disabled-color:                 $pagination-disabled-color !default;
+
+
+//== Jumbotron
+//
+//##
+
+$jumbotron-padding:              30px !default;
+$jumbotron-color:                inherit !default;
+$jumbotron-bg:                   $gray-lighter !default;
+$jumbotron-heading-color:        inherit !default;
+$jumbotron-font-size:            ceil(($font-size-base * 1.5)) !default;
+$jumbotron-heading-font-size:    ceil(($font-size-base * 4.5)) !default;
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+$state-success-text:             #3c763d !default;
+$state-success-bg:               #dff0d8 !default;
+$state-success-border:           darken(adjust-hue($state-success-bg, -10), 5%) !default;
+
+$state-info-text:                #31708f !default;
+$state-info-bg:                  #d9edf7 !default;
+$state-info-border:              darken(adjust-hue($state-info-bg, -10), 7%) !default;
+
+$state-warning-text:             #8a6d3b !default;
+$state-warning-bg:               #fcf8e3 !default;
+$state-warning-border:           darken(adjust-hue($state-warning-bg, -10), 5%) !default;
+
+$state-danger-text:              #a94442 !default;
+$state-danger-bg:                #f2dede !default;
+$state-danger-border:            darken(adjust-hue($state-danger-bg, -10), 5%) !default;
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+$tooltip-max-width:           200px !default;
+//** Tooltip text color
+$tooltip-color:               #fff !default;
+//** Tooltip background color
+$tooltip-bg:                  #000 !default;
+$tooltip-opacity:             .9 !default;
+
+//** Tooltip arrow width
+$tooltip-arrow-width:         5px !default;
+//** Tooltip arrow color
+$tooltip-arrow-color:         $tooltip-bg !default;
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+$popover-bg:                          #fff !default;
+//** Popover maximum width
+$popover-max-width:                   276px !default;
+//** Popover border color
+$popover-border-color:                rgba(0,0,0,.2) !default;
+//** Popover fallback border color
+$popover-fallback-border-color:       #ccc !default;
+
+//** Popover title background color
+$popover-title-bg:                    darken($popover-bg, 3%) !default;
+
+//** Popover arrow width
+$popover-arrow-width:                 10px !default;
+//** Popover arrow color
+$popover-arrow-color:                 $popover-bg !default;
+
+//** Popover outer arrow width
+$popover-arrow-outer-width:           ($popover-arrow-width + 1) !default;
+//** Popover outer arrow color
+$popover-arrow-outer-color:           fade_in($popover-border-color, 0.05) !default;
+//** Popover outer arrow fallback color
+$popover-arrow-outer-fallback-color:  darken($popover-fallback-border-color, 20%) !default;
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+$label-default-bg:            $gray-light !default;
+//** Primary label background color
+$label-primary-bg:            $brand-primary !default;
+//** Success label background color
+$label-success-bg:            $brand-success !default;
+//** Info label background color
+$label-info-bg:               $brand-info !default;
+//** Warning label background color
+$label-warning-bg:            $brand-warning !default;
+//** Danger label background color
+$label-danger-bg:             $brand-danger !default;
+
+//** Default label text color
+$label-color:                 #fff !default;
+//** Default text color of a linked label
+$label-link-hover-color:      #fff !default;
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+$modal-inner-padding:         15px !default;
+
+//** Padding applied to the modal title
+$modal-title-padding:         15px !default;
+//** Modal title line-height
+$modal-title-line-height:     $line-height-base !default;
+
+//** Background color of modal content area
+$modal-content-bg:                             #fff !default;
+//** Modal content border color
+$modal-content-border-color:                   rgba(0,0,0,.2) !default;
+//** Modal content border color **for IE8**
+$modal-content-fallback-border-color:          #999 !default;
+
+//** Modal backdrop background color
+$modal-backdrop-bg:           #000 !default;
+//** Modal backdrop opacity
+$modal-backdrop-opacity:      .5 !default;
+//** Modal header border color
+$modal-header-border-color:   #e5e5e5 !default;
+//** Modal footer border color
+$modal-footer-border-color:   $modal-header-border-color !default;
+
+$modal-lg:                    900px !default;
+$modal-md:                    600px !default;
+$modal-sm:                    300px !default;
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+$alert-padding:               15px !default;
+$alert-border-radius:         $border-radius-base !default;
+$alert-link-font-weight:      bold !default;
+
+$alert-success-bg:            $state-success-bg !default;
+$alert-success-text:          $state-success-text !default;
+$alert-success-border:        $state-success-border !default;
+
+$alert-info-bg:               $state-info-bg !default;
+$alert-info-text:             $state-info-text !default;
+$alert-info-border:           $state-info-border !default;
+
+$alert-warning-bg:            $state-warning-bg !default;
+$alert-warning-text:          $state-warning-text !default;
+$alert-warning-border:        $state-warning-border !default;
+
+$alert-danger-bg:             $state-danger-bg !default;
+$alert-danger-text:           $state-danger-text !default;
+$alert-danger-border:         $state-danger-border !default;
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+$progress-bg:                 #f5f5f5 !default;
+//** Progress bar text color
+$progress-bar-color:          #fff !default;
+//** Variable for setting rounded corners on progress bar.
+$progress-border-radius:      $border-radius-base !default;
+
+//** Default progress bar color
+$progress-bar-bg:             $brand-primary !default;
+//** Success progress bar color
+$progress-bar-success-bg:     $brand-success !default;
+//** Warning progress bar color
+$progress-bar-warning-bg:     $brand-warning !default;
+//** Danger progress bar color
+$progress-bar-danger-bg:      $brand-danger !default;
+//** Info progress bar color
+$progress-bar-info-bg:        $brand-info !default;
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+$list-group-bg:                 #fff !default;
+//** `.list-group-item` border color
+$list-group-border:             #ddd !default;
+//** List group border radius
+$list-group-border-radius:      $border-radius-base !default;
+
+//** Background color of single list items on hover
+$list-group-hover-bg:           #f5f5f5 !default;
+//** Text color of active list items
+$list-group-active-color:       $component-active-color !default;
+//** Background color of active list items
+$list-group-active-bg:          $component-active-bg !default;
+//** Border color of active list elements
+$list-group-active-border:      $list-group-active-bg !default;
+//** Text color for content within active list items
+$list-group-active-text-color:  lighten($list-group-active-bg, 40%) !default;
+
+//** Text color of disabled list items
+$list-group-disabled-color:      $gray-light !default;
+//** Background color of disabled list items
+$list-group-disabled-bg:         $gray-lighter !default;
+//** Text color for content within disabled list items
+$list-group-disabled-text-color: $list-group-disabled-color !default;
+
+$list-group-link-color:         #555 !default;
+$list-group-link-hover-color:   $list-group-link-color !default;
+$list-group-link-heading-color: #333 !default;
+
+
+//== Panels
+//
+//##
+
+$panel-bg:                    #fff !default;
+$panel-body-padding:          15px !default;
+$panel-heading-padding:       10px 15px !default;
+$panel-footer-padding:        $panel-heading-padding !default;
+$panel-border-radius:         $border-radius-base !default;
+
+//** Border color for elements within panels
+$panel-inner-border:          #ddd !default;
+$panel-footer-bg:             #f5f5f5 !default;
+
+$panel-default-text:          $gray-dark !default;
+$panel-default-border:        #ddd !default;
+$panel-default-heading-bg:    #f5f5f5 !default;
+
+$panel-primary-text:          #fff !default;
+$panel-primary-border:        $brand-primary !default;
+$panel-primary-heading-bg:    $brand-primary !default;
+
+$panel-success-text:          $state-success-text !default;
+$panel-success-border:        $state-success-border !default;
+$panel-success-heading-bg:    $state-success-bg !default;
+
+$panel-info-text:             $state-info-text !default;
+$panel-info-border:           $state-info-border !default;
+$panel-info-heading-bg:       $state-info-bg !default;
+
+$panel-warning-text:          $state-warning-text !default;
+$panel-warning-border:        $state-warning-border !default;
+$panel-warning-heading-bg:    $state-warning-bg !default;
+
+$panel-danger-text:           $state-danger-text !default;
+$panel-danger-border:         $state-danger-border !default;
+$panel-danger-heading-bg:     $state-danger-bg !default;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+$thumbnail-padding:           4px !default;
+//** Thumbnail background color
+$thumbnail-bg:                $body-bg !default;
+//** Thumbnail border color
+$thumbnail-border:            #ddd !default;
+//** Thumbnail border radius
+$thumbnail-border-radius:     $border-radius-base !default;
+
+//** Custom text color for thumbnail captions
+$thumbnail-caption-color:     $text-color !default;
+//** Padding around the thumbnail caption
+$thumbnail-caption-padding:   9px !default;
+
+
+//== Wells
+//
+//##
+
+$well-bg:                     #f5f5f5 !default;
+$well-border:                 darken($well-bg, 7%) !default;
+
+
+//== Badges
+//
+//##
+
+$badge-color:                 #fff !default;
+//** Linked badge text color on hover
+$badge-link-hover-color:      #fff !default;
+$badge-bg:                    $gray-light !default;
+
+//** Badge text color in active nav link
+$badge-active-color:          $link-color !default;
+//** Badge background color in active nav link
+$badge-active-bg:             #fff !default;
+
+$badge-font-weight:           bold !default;
+$badge-line-height:           1 !default;
+$badge-border-radius:         10px !default;
+
+
+//== Breadcrumbs
+//
+//##
+
+$breadcrumb-padding-vertical:   8px !default;
+$breadcrumb-padding-horizontal: 15px !default;
+//** Breadcrumb background color
+$breadcrumb-bg:                 #f5f5f5 !default;
+//** Breadcrumb text color
+$breadcrumb-color:              #ccc !default;
+//** Text color of current page in the breadcrumb
+$breadcrumb-active-color:       $gray-light !default;
+//** Textual separator for between breadcrumb elements
+$breadcrumb-separator:          "/" !default;
+
+
+//== Carousel
+//
+//##
+
+$carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6) !default;
+
+$carousel-control-color:                      #fff !default;
+$carousel-control-width:                      15% !default;
+$carousel-control-opacity:                    .5 !default;
+$carousel-control-font-size:                  20px !default;
+
+$carousel-indicator-active-bg:                #fff !default;
+$carousel-indicator-border-color:             #fff !default;
+
+$carousel-caption-color:                      #fff !default;
+
+
+//== Close
+//
+//##
+
+$close-font-weight:           bold !default;
+$close-color:                 #000 !default;
+$close-text-shadow:           0 1px 0 #fff !default;
+
+
+//== Code
+//
+//##
+
+$code-color:                  #c7254e !default;
+$code-bg:                     #f9f2f4 !default;
+
+$kbd-color:                   #fff !default;
+$kbd-bg:                      #333 !default;
+
+$pre-bg:                      #f5f5f5 !default;
+$pre-color:                   $gray-dark !default;
+$pre-border-color:            #ccc !default;
+$pre-scrollable-max-height:   340px !default;
+
+
+//== Type
+//
+//##
+
+//** Horizontal offset for forms and lists.
+$component-offset-horizontal: 180px !default;
+//** Text muted color
+$text-muted:                  $gray-light !default;
+//** Abbreviations and acronyms border color
+$abbr-border-color:           $gray-light !default;
+//** Headings small color
+$headings-small-color:        $gray-light !default;
+//** Blockquote small color
+$blockquote-small-color:      $gray-light !default;
+//** Blockquote font size
+$blockquote-font-size:        ($font-size-base * 1.25) !default;
+//** Blockquote border color
+$blockquote-border-color:     $gray-lighter !default;
+//** Page header border color
+$page-header-border-color:    $gray-lighter !default;
+//** Width of horizontal description list titles
+$dl-horizontal-offset:        $component-offset-horizontal !default;
+//** Point at which .dl-horizontal becomes horizontal
+$dl-horizontal-breakpoint:    $grid-float-breakpoint !default;
+//** Horizontal line color.
+$hr-border:                   $gray-lighter !default;
diff --git a/app/l8pr/static/styles/theme/_login.scss b/app/l8pr/static/styles/theme/_login.scss
index e7ff409..e69de29 100644
--- a/app/l8pr/static/styles/theme/_login.scss
+++ b/app/l8pr/static/styles/theme/_login.scss
@@ -1,7 +0,0 @@
-.login {
-  .login-container {
-    max-width: 600px;
-    margin: 0 auto;
-    padding: 20px;
-  }
-}
\ No newline at end of file
diff --git a/app/settings.py b/app/settings.py
index 8532794..86347f5 100644
--- a/app/settings.py
+++ b/app/settings.py
@@ -40,9 +40,10 @@
     'webpack_loader',
     # 'compressor',
     'rest_framework',
+    'rest_framework.authtoken',
     'oauth2_provider',
     # 'social.apps.django_app.default',
-    'rest_framework_social_oauth2',
+    # 'rest_framework_social_oauth2',
     'djoser',
     'haystack',
     'drf_haystack',
@@ -166,7 +167,8 @@
         # 'oauth2_provider.ext.rest_framework.OAuth2Authentication',
         # 'rest_framework_social_oauth2.authentication.SocialAuthentication',
         # 'rest_framework.authentication.BasicAuthentication',
-        'rest_framework.authentication.SessionAuthentication',
+        # 'rest_framework.authentication.SessionAuthentication',
+        'rest_framework.authentication.TokenAuthentication',
     ),
     'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
     'DEFAULT_FILTER_BACKENDS': (
@@ -187,7 +189,7 @@
 AUTHENTICATION_BACKENDS = (
     # Facebook OAuth2
     # django-rest-framework-social-oauth2
-    'rest_framework_social_oauth2.backends.DjangoOAuth2',
+    # 'rest_framework_social_oauth2.backends.DjangoOAuth2',
     # Django
     'django.contrib.auth.backends.ModelBackend',
 )
@@ -240,8 +242,6 @@
 TWITTER_CONSUMER_SECRET = os.environ.get('TWITTER_CONSUMER_SECRET')
 IFRAMELY_API_KEY = os.environ.get('IFRAMELY_API_KEY')
 
-import os
-
 LOGGING = {
     'version': 1,
     'disable_existing_loggers': False,
diff --git a/app/urls.py b/app/urls.py
index fca21cd..993007c 100644
--- a/app/urls.py
+++ b/app/urls.py
@@ -39,10 +39,11 @@
     url(r'^api/', include(router.urls)),
     url(r'^api/metadata/', MetadataView.as_view()),
     url(r'^api/youtube/', SearchYoutubeView.as_view()),
-    url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
-    url(r'^api/auth/logout/$', auth_views.logout),
-    url(r'^api/register/', include('djoser.urls')),
-    url(r'^auth/', include('rest_framework_social_oauth2.urls')),
+    # url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
+    # url(r'^api/auth/logout/$', auth_views.logout),
+    url(r'^auth/', include('djoser.urls.authtoken')),
+    # url(r'^auth/', include('djoser.urls')),
+    # url(r'^auth/', include('rest_framework_social_oauth2.urls')),
     url(r'^admin/', admin.site.urls),
     url(r'^open/.+$', HomePageView.as_view(template_name='index.html')),
     # url(r'^(?P\w+)$', HomePageView.as_view(template_name='index.html')),
diff --git a/package.json b/package.json
index c5b0bbc..a962990 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "postcss-import": "9.0.0",
     "postcss-loader": "1.2.0",
     "react": "15.4.1",
+    "react-bootstrap": "^0.30.8",
     "react-dom": "15.4.1",
     "react-mixin": "3.0.5",
     "react-player": "^0.14.3",
diff --git a/requirements.txt b/requirements.txt
index 1e6aafa..388c74e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,13 +10,14 @@ six==1.10.0
 wheel==0.24.0
 google-api-python-client==1.5.0
 soundcloud==0.5.0
-django-rest-framework-social-oauth2==1.0.5
+# django-rest-framework-social-oauth2==1.0.5
 whitenoise==2.0.6
 git+https://github.com/django-haystack/django-haystack.git@cc0974d53de637c16468b6779d486794ac9caaf5
 elasticsearch==2.3.0
-djoser==0.5.0
+# djoser==0.5.0
 drf-haystack==1.5.6
 twython==3.4.0
+django-rest-knox==2.2.0
 # PROD
 gunicorn==19.4.5
 dj-database-url==0.4.0

From 6337e2f78fa48e7e9aa1cd9253f1be045d592c50 Mon Sep 17 00:00:00 2001
From: Edouard Richard 
Date: Wed, 12 Apr 2017 10:09:26 +0200
Subject: [PATCH 006/143] no bower

---
 .bowerrc           |  3 ---
 app/l8pr/views.py  | 15 ---------------
 bin/_install_less  |  6 ------
 bin/install_nodejs |  5 -----
 bower.json         | 40 ----------------------------------------
 5 files changed, 69 deletions(-)
 delete mode 100644 .bowerrc
 delete mode 100644 bin/_install_less
 delete mode 100644 bower.json

diff --git a/.bowerrc b/.bowerrc
deleted file mode 100644
index 0666e13..0000000
--- a/.bowerrc
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "directory": "app/l8pr/static/bower_components"
-}
diff --git a/app/l8pr/views.py b/app/l8pr/views.py
index 06e4f9a..90893e3 100644
--- a/app/l8pr/views.py
+++ b/app/l8pr/views.py
@@ -11,19 +11,6 @@
 from django.http import HttpResponse
 
 
-def angular_templates():
-    partials_dir = settings.STATICFILES_DIRS[0]
-    exclude = ('bower_components',)
-    for (root, dirs, files) in os.walk(partials_dir, topdown=True):
-        dirs[:] = [d for d in dirs if d not in exclude]
-        for file_name in files:
-            if file_name.endswith('.html'):
-                file_path = os.path.join(root, file_name)
-                with open(file_path, 'rb') as fh:
-                    file_name = file_path[len(partials_dir) + 1:]
-                    yield (file_name, normalize_newlines(fh.read().decode('utf-8')).replace('\n', ' '))
-
-
 class IndexView(View):
     """Render main page."""
 
@@ -65,8 +52,6 @@ def get_context_data(self, **kwargs):
         else:
             context['title'] = 'l8pr'
             context['thumbnail'] = self.request.build_absolute_uri('static/images/L8PRtv.png')
-        # add templates
-        context['templates'] = angular_templates()
         # add GA
         context['GA'] = os.environ.get('GA')
         return context
diff --git a/bin/_install_less b/bin/_install_less
deleted file mode 100644
index 1c03f43..0000000
--- a/bin/_install_less
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-set -eo pipefail
-
-
-npm install
-bower install
diff --git a/bin/install_nodejs b/bin/install_nodejs
index 37faae7..f4cebf7 100644
--- a/bin/install_nodejs
+++ b/bin/install_nodejs
@@ -25,8 +25,3 @@ popd
 ln -s -f ../../vendor/node/bin/node .heroku/python/bin/node
 ln -s -f ../../vendor/node/bin/node-waf .heroku/python/bin/node-waf
 ln -s -f ../../vendor/node/bin/npm .heroku/python/bin/npm
-
-
-npm install -g less
-npm install -g bower
-/app/.heroku/vendor/node/lib/node_modules/bower/bin/bower install
diff --git a/bower.json b/bower.json
deleted file mode 100644
index 23e42da..0000000
--- a/bower.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
-  "name": "L8pr",
-  "version": "0.0.0",
-  "authors": [
-    "Edouard "
-  ],
-  "license": "MIT",
-  "ignore": [
-    "**/.*",
-    "node_modules",
-    "static/bower_components",
-    "test",
-    "tests"
-  ],
-  "dependencies": {
-    "modernizr": "~2.8.3",
-    "angular": "~1.5.0",
-    "angular-resource": "~1.5.0",
-    "restangular": "~1.5.1",
-    "angular-sanitize": "~1.5.0",
-    "angular-local-storage": "~0.2.2",
-    "components-font-awesome": "~4.3.0",
-    "jquery": "~2.1.4",
-    "lodash": "~3.10.0",
-    "angular-hotkeys": "chieffancypants/angular-hotkeys#~1.4.5",
-    "angular-animate": "~1.5.0",
-    "angular-fullscreen": "~1.0.1",
-    "angular-perfect-scrollbar": "~0.0.4",
-    "bootstrap": "~3.3.6",
-    "angular-ui-router": "~0.3.0",
-    "angular-bootstrap": "~1.3.2",
-    "plyr": "~1.6.16",
-    "webtorrent": "~0.94.4",
-    "jquery-mousewheel": "~3.1.13",
-    "angular-confirm-modal": "~1.2.5"
-  },
-  "resolutions": {
-    "angular": "1.5.6"
-  }
-}

From 2527deea096bddc08330239c2de5de92ede1f300 Mon Sep 17 00:00:00 2001
From: Edouard Richard 
Date: Wed, 12 Apr 2017 10:09:40 +0200
Subject: [PATCH 007/143] localstorage

---
 app/l8pr/static/actions/auth.js   | 10 ++++-----
 app/l8pr/static/actions/player.js | 15 +++++++-------
 app/l8pr/static/index.js          | 34 +++++++++++++++----------------
 3 files changed, 30 insertions(+), 29 deletions(-)

diff --git a/app/l8pr/static/actions/auth.js b/app/l8pr/static/actions/auth.js
index 30ef9c9..36a2f9e 100644
--- a/app/l8pr/static/actions/auth.js
+++ b/app/l8pr/static/actions/auth.js
@@ -11,8 +11,8 @@ import {
 } from '../constants'
 
 export function authLoginUserSuccess(token, user) {
-    sessionStorage.setItem('token', token)
-    sessionStorage.setItem('user', JSON.stringify(user))
+    localStorage.setItem('token', token)
+    localStorage.setItem('user', JSON.stringify(user))
     return {
         type: AUTH_LOGIN_USER_SUCCESS,
         payload: {
@@ -23,7 +23,7 @@ export function authLoginUserSuccess(token, user) {
 }
 
 export function authLoginUserFailure(error, message) {
-    sessionStorage.removeItem('token')
+    localStorage.removeItem('token')
     return {
         type: AUTH_LOGIN_USER_FAILURE,
         payload: {
@@ -38,8 +38,8 @@ export function authLoginUserRequest() {
 }
 
 export function authLogout() {
-    sessionStorage.removeItem('token')
-    sessionStorage.removeItem('user')
+    localStorage.removeItem('token')
+    localStorage.removeItem('user')
     return { type: AUTH_LOGOUT_USER }
 }
 
diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js
index 39e67c9..67133c0 100644
--- a/app/l8pr/static/actions/player.js
+++ b/app/l8pr/static/actions/player.js
@@ -11,17 +11,18 @@ export function setPlaylist(playlist) {
 }
 
 export function initQueueList() {
-    return (dispatch) => (
-        Promise.all([
+    return (dispatch, getState) => {
+        const currentUserId = getState().auth.user.id
+        return Promise.all([
             // LAST ITEMS
-            fetchLastItems({ user: 1 })
+            fetchLastItems({ user: currentUserId })
             // set context
             .then((items) => (items.map((i) => ({
                 ...i,
                 context: { title: 'Last Items' },
             })))),
             // SHOWS
-            fetchUserShows({ user: 1 })
+            fetchUserShows({ user: currentUserId })
             .then((shows) => (
                 shows.map((show) => (
                     // set context
@@ -45,7 +46,7 @@ export function initQueueList() {
         .then(() => dispatch(next()))
         // start the player
         .then(() => dispatch(play()))
-    )
+    }
 }
 
 export function play(showAndItem) {
@@ -58,9 +59,9 @@ export function play(showAndItem) {
         const item = selectors.currentTrack(getState())
         let url = ''
         if (show) {
-            url += `/show/${show}`
+            url += `/show/${show.id}`
         } if (item) {
-            url += `/item/${item}`
+            url += `/item/${item.id}`
         }
         dispatch(push(url))
     }
diff --git a/app/l8pr/static/index.js b/app/l8pr/static/index.js
index 98180b4..77fcc46 100644
--- a/app/l8pr/static/index.js
+++ b/app/l8pr/static/index.js
@@ -1,33 +1,33 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import { browserHistory } from 'react-router';
-import { syncHistoryWithStore } from 'react-router-redux';
+import React from 'react'
+import ReactDOM from 'react-dom'
+import { browserHistory } from 'react-router'
+import { syncHistoryWithStore } from 'react-router-redux'
 
-import Root from './containers/Root/Root';
-import configureStore from './store/configureStore';
-import { authLoginUserSuccess } from './actions/auth';
+import Root from './containers/Root/Root'
+import configureStore from './store/configureStore'
+import { authLoginUserSuccess } from './actions/auth'
 
 
-const initialState = {};
-const target = document.getElementById('root');
+const initialState = {}
+const target = document.getElementById('root')
 
-const store = configureStore(initialState, browserHistory);
-const history = syncHistoryWithStore(browserHistory, store);
+const store = configureStore(initialState, browserHistory)
+const history = syncHistoryWithStore(browserHistory, store)
 
 const node = (
     
-);
+)
 
-const token = sessionStorage.getItem('token');
-let user = {};
+const token = localStorage.getItem('token')
+let user = {}
 try {
-    user = JSON.parse(sessionStorage.getItem('user'));
+    user = JSON.parse(localStorage.getItem('user'))
 } catch (e) {
     // Failed to parse
 }
 
 if (token !== null) {
-    store.dispatch(authLoginUserSuccess(token, user));
+    store.dispatch(authLoginUserSuccess(token, user))
 }
 
-ReactDOM.render(node, target);
+ReactDOM.render(node, target)

From 78816802be3dda176b973e2b29168ff08540356d Mon Sep 17 00:00:00 2001
From: Edouard Richard 
Date: Wed, 12 Apr 2017 13:17:26 +0200
Subject: [PATCH 008/143] list

---
 app/l8pr/static/actions/player.js             | 14 +++--
 app/l8pr/static/components/ListItem/index.js  | 23 +++++++
 .../static/components/ListItem/style.scss     |  4 ++
 app/l8pr/static/components/Loop/index.js      | 43 -------------
 app/l8pr/static/components/Loop/style.scss    | 21 -------
 app/l8pr/static/components/LoopItem/index.js  | 20 ------
 .../static/components/LoopItem/style.scss     |  2 -
 app/l8pr/static/components/PlayQueue/index.js | 63 +++++++++++++++++++
 .../static/components/PlayQueue/style.scss    |  8 +++
 app/l8pr/static/components/PlayQueue/test.js  |  0
 app/l8pr/static/components/index.js           |  4 +-
 app/l8pr/static/constants/index.js            |  1 +
 app/l8pr/static/containers/Strip/index.js     | 17 +++--
 app/l8pr/static/reducers/player.js            | 21 +++++--
 app/settings.py                               | 12 +++-
 package.json                                  |  1 +
 16 files changed, 148 insertions(+), 106 deletions(-)
 create mode 100644 app/l8pr/static/components/ListItem/index.js
 create mode 100644 app/l8pr/static/components/ListItem/style.scss
 delete mode 100644 app/l8pr/static/components/Loop/index.js
 delete mode 100644 app/l8pr/static/components/Loop/style.scss
 delete mode 100644 app/l8pr/static/components/LoopItem/index.js
 delete mode 100644 app/l8pr/static/components/LoopItem/style.scss
 create mode 100644 app/l8pr/static/components/PlayQueue/index.js
 create mode 100644 app/l8pr/static/components/PlayQueue/style.scss
 create mode 100644 app/l8pr/static/components/PlayQueue/test.js

diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js
index 67133c0..8d6a4dd 100644
--- a/app/l8pr/static/actions/player.js
+++ b/app/l8pr/static/actions/player.js
@@ -49,12 +49,16 @@ export function initQueueList() {
     }
 }
 
-export function play(showAndItem) {
+export function playItem(item) {
+    return {
+        type: c.SET_CURRENT,
+        payload: item,
+    }
+}
+
+export function play() {
     return (dispatch, getState) => {
-        dispatch({
-            type: c.PLAY,
-            payload: showAndItem,
-        })
+        dispatch({ type: c.PLAY })
         const show = selectors.currentShow(getState())
         const item = selectors.currentTrack(getState())
         let url = ''
diff --git a/app/l8pr/static/components/ListItem/index.js b/app/l8pr/static/components/ListItem/index.js
new file mode 100644
index 0000000..9f02c8b
--- /dev/null
+++ b/app/l8pr/static/components/ListItem/index.js
@@ -0,0 +1,23 @@
+import React from 'react'
+import moment from 'moment'
+import './style.scss'
+
+export default function ListItem({ item, onPlayClick }) {
+    return (
+        
+
+ {item.title} + {moment.duration(item.duration, 's').humanize()} +
{item.description}
+
+
+ +
+
+ ) +} + +ListItem.propTypes = { + item: React.PropTypes.object.isRequired, + onPlayClick: React.PropTypes.func.isRequired, +} diff --git a/app/l8pr/static/components/ListItem/style.scss b/app/l8pr/static/components/ListItem/style.scss new file mode 100644 index 0000000..0cb28e7 --- /dev/null +++ b/app/l8pr/static/components/ListItem/style.scss @@ -0,0 +1,4 @@ +.ListItem__thumbnail { + width: 50px; + height: 50px; +} diff --git a/app/l8pr/static/components/Loop/index.js b/app/l8pr/static/components/Loop/index.js deleted file mode 100644 index 797fc56..0000000 --- a/app/l8pr/static/components/Loop/index.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react' -import { LoopItem } from '../index' -import './style.scss' - -export default class Loop extends React.Component { - constructor(props) { - super() - this.state = { - offset: 0, - perPage: 3, - } - } - previous() { - this.setState({ offset: this.state.offset - (this.state.perPage - 1) }) - } - next() { - this.setState({ offset: this.state.offset + (this.state.perPage - 1) }) - } - render() { - const items = this.props.items.slice(this.state.offset, this.state.offset + this.state.perPage) - return ( -
-
- navigate_before -
-
    - {items.map(i => ( -
  • - -
  • - ))} -
-
- navigate_next -
-
- ) - } -} - -Loop.propTypes = { - items: React.PropTypes.array.isRequired -} diff --git a/app/l8pr/static/components/Loop/style.scss b/app/l8pr/static/components/Loop/style.scss deleted file mode 100644 index 7d26a88..0000000 --- a/app/l8pr/static/components/Loop/style.scss +++ /dev/null @@ -1,21 +0,0 @@ -.Loop { - position: relative; - white-space: nowrap; - height: 200px; - li { - display: inline-block; - height: 200px; - overflow: hidden; - } - .Loop__nav { - position: absolute; - top: 50; - bottom: 0; - } - .Loop__nav--prev { - left: 0; - } - .Loop__nav--next { - right: 0; - } -} diff --git a/app/l8pr/static/components/LoopItem/index.js b/app/l8pr/static/components/LoopItem/index.js deleted file mode 100644 index 4f9c483..0000000 --- a/app/l8pr/static/components/LoopItem/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' -import './style.scss' - -export default function LoopItem({ item }) { - return ( -
-
-

{item.title}

- {item.description} -
-
- -
-
- ) -} - -LoopItem.propTypes = { - item: React.PropTypes.object.isRequired -} diff --git a/app/l8pr/static/components/LoopItem/style.scss b/app/l8pr/static/components/LoopItem/style.scss deleted file mode 100644 index 3a417c5..0000000 --- a/app/l8pr/static/components/LoopItem/style.scss +++ /dev/null @@ -1,2 +0,0 @@ -.LoopItem { -} diff --git a/app/l8pr/static/components/PlayQueue/index.js b/app/l8pr/static/components/PlayQueue/index.js new file mode 100644 index 0000000..028821a --- /dev/null +++ b/app/l8pr/static/components/PlayQueue/index.js @@ -0,0 +1,63 @@ +import React from 'react' +import { ListItem } from '../index' +import { get } from 'lodash' +import moment from 'moment' + +import './style.scss' + +export default class PlayQueue extends React.Component { + constructor(props) { + super(props) + } + render() { + const { items, onItemPlayClick } = this.props + const contexts = groupByContext(items) + return ( +
+
    + {contexts.map((c) => ( +
  • +
    {c.context.title}
    +
    {moment.duration(getDuration(c.items), 's').humanize()}
    +
      + {c.items.map(i => ( +
    1. + +
    2. + ))} +
    +
  • + ))} +
+
+ ) + } +} + +PlayQueue.propTypes = { + items: React.PropTypes.array.isRequired, + onItemPlayClick: React.PropTypes.func.isRequired, +} + +function groupByContext(items) { + const contexts = [{ + context: items[0].context, + items: [], + }] + items.forEach((i) => { + const lastContext = contexts[contexts.length - 1] + if (i.context.id !== get(lastContext, 'context.id')) { + contexts.push({ + context: null, + items: [], + }) + } + if (contexts[contexts.length - 1].context === null) contexts[contexts.length - 1].context = i.context + contexts[contexts.length - 1].items.push(i) + }) + return contexts +} + +function getDuration(items) { + return items.reduce((r, i) => (r + i.duration), 0) +} diff --git a/app/l8pr/static/components/PlayQueue/style.scss b/app/l8pr/static/components/PlayQueue/style.scss new file mode 100644 index 0000000..12f056b --- /dev/null +++ b/app/l8pr/static/components/PlayQueue/style.scss @@ -0,0 +1,8 @@ +.PlayQueue { + max-height: 74vh; + overflow: auto; + background-color: white; +} +.context__title { + font-size: 1.25em; +} diff --git a/app/l8pr/static/components/PlayQueue/test.js b/app/l8pr/static/components/PlayQueue/test.js new file mode 100644 index 0000000..e69de29 diff --git a/app/l8pr/static/components/index.js b/app/l8pr/static/components/index.js index d234979..c62be82 100644 --- a/app/l8pr/static/components/index.js +++ b/app/l8pr/static/components/index.js @@ -1,4 +1,4 @@ export Screen from './Screen' -export Loop from './Loop' -export LoopItem from './LoopItem' +export PlayQueue from './PlayQueue' +export ListItem from './ListItem' export LoginModal from './LoginModal' diff --git a/app/l8pr/static/constants/index.js b/app/l8pr/static/constants/index.js index 599b47a..5dae38c 100644 --- a/app/l8pr/static/constants/index.js +++ b/app/l8pr/static/constants/index.js @@ -6,6 +6,7 @@ export const AUTH_LOGOUT_USER = 'AUTH_LOGOUT_USER' export const DATA_FETCH_PROTECTED_DATA_REQUEST = 'DATA_FETCH_PROTECTED_DATA_REQUEST' export const DATA_RECEIVE_PROTECTED_DATA = 'DATA_RECEIVE_PROTECTED_DATA' +export const SET_CURRENT = 'SET_CURRENT' export const SET_LOOP = 'SET_LOOP' export const SET_SHOWS = 'SET_SHOWS' export const PLAY_ITEM = 'PLAY_ITEM' diff --git a/app/l8pr/static/containers/Strip/index.js b/app/l8pr/static/containers/Strip/index.js index 32a9aff..74ed437 100644 --- a/app/l8pr/static/containers/Strip/index.js +++ b/app/l8pr/static/containers/Strip/index.js @@ -1,22 +1,24 @@ import React from 'react' import { connect } from 'react-redux' -import { Loop } from '../../components' +import { PlayQueue } from '../../components' import { StripHeader, Controller } from '../index' +import * as player from '../../actions/player' import './style.scss' class Strip extends React.Component { static propTypes = { stripOpened: React.PropTypes.bool, - loopItems: React.PropTypes.array, + playlist: React.PropTypes.array, + onItemPlayClick: React.PropTypes.func, } render() { - const { stripOpened, loopItems } = this.props + const { stripOpened, playlist, onItemPlayClick } = this.props return (
{ stripOpened && - + }
@@ -26,6 +28,9 @@ class Strip extends React.Component { const mapStateToProps = (state) => ({ stripOpened: state.browser.stripOpened, - loopItems: state.browser.loop, + playlist: [state.player.current, ...state.player.playQueue], }) -export default connect(mapStateToProps)(Strip) +const mapDispatchToProps = (dispatch) => ({ + onItemPlayClick: (item) => (dispatch(player.playItem(item))), +}) +export default connect(mapStateToProps, mapDispatchToProps)(Strip) diff --git a/app/l8pr/static/reducers/player.js b/app/l8pr/static/reducers/player.js index 7bb8fe0..493ba04 100644 --- a/app/l8pr/static/reducers/player.js +++ b/app/l8pr/static/reducers/player.js @@ -1,5 +1,8 @@ import * as c from '../constants' -import { get } from 'lodash' +import { get, reject } from 'lodash' + + +const rejectNull = (source) => reject(source, (i) => i === null) export default function (state = { current: null, @@ -9,6 +12,14 @@ export default function (state = { playing: false, }, action = null) { switch (action.type) { + case c.SET_CURRENT: + return { + ...state, + current: action.payload, + history: [state.current, ...state.history], + // remove what was before the item + playQueue: state.playQueue.slice(state.playQueue.indexOf(action.payload) + 1), + } case c.NEXT: var playQueue = [...state.playQueue] var next = playQueue.shift() @@ -16,7 +27,7 @@ export default function (state = { ...state, current: next, playQueue, - history: [state.current, ...state.history], + history: rejectNull([state.current, ...state.history]), } case c.PREVIOUS: var history = [...state.history] @@ -24,7 +35,7 @@ export default function (state = { return { ...state, current: previous, - playQueue: [state.current, ...state.playQueue], + playQueue: rejectNull([state.current, ...state.playQueue]), history, } case c.NEXT_CONTEXT: @@ -35,7 +46,7 @@ export default function (state = { ...state, current: state.playQueue[nextIndex], playQueue: state.playQueue.slice(nextIndex + 1), - history: [...state.playQueue.slice(0, nextIndex).reverse(), ...state.history], + history: rejectNull([...state.playQueue.slice(0, nextIndex).reverse(), state.current, ...state.history]), } case c.PREVIOUS_CONTEXT: var previousIndex = state.history.findIndex( @@ -45,7 +56,7 @@ export default function (state = { ...state, current: state.history[previousIndex], history: state.history.slice(previousIndex + 1), - playQueue: [...state.history.slice(0, previousIndex).reverse(), ...state.playQueue], + playQueue: rejectNull([...state.history.slice(0, previousIndex).reverse(), state.current, ...state.playQueue]), } case c.SET_PLAYLIST: return { diff --git a/app/settings.py b/app/settings.py index 86347f5..d55bc59 100644 --- a/app/settings.py +++ b/app/settings.py @@ -88,9 +88,17 @@ # https://docs.djangoproject.com/en/1.9/ref/settings/#databases DATABASES = { + # 'default': { + # 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + # } 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'loopr', + 'USER': 'postgres', + 'PASSWORD': 'mysecretpassword', + 'HOST': '172.17.0.2', + 'PORT': '5432', } } diff --git a/package.json b/package.json index a962990..d66cb88 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "less-loader": "2.2.3", "material-design-icons": "^3.0.1", "method-override": "2.3.7", + "moment": "^2.18.1", "node-sass": "3.13.1", "postcss-import": "9.0.0", "postcss-loader": "1.2.0", From cb067f17dabc6f571e20cb79f63bb2460b57ec31 Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Wed, 12 Apr 2017 15:12:25 +0200 Subject: [PATCH 009/143] djoser in requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 388c74e..2dbe3ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ soundcloud==0.5.0 whitenoise==2.0.6 git+https://github.com/django-haystack/django-haystack.git@cc0974d53de637c16468b6779d486794ac9caaf5 elasticsearch==2.3.0 -# djoser==0.5.0 +djoser==0.5.0 drf-haystack==1.5.6 twython==3.4.0 django-rest-knox==2.2.0 From f75219db05da073f63ae055b5a42df5fdcb83517 Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Wed, 12 Apr 2017 15:48:20 +0200 Subject: [PATCH 010/143] fix no auth --- app/l8pr/static/actions/player.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index 8d6a4dd..d13a901 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -1,6 +1,7 @@ import { fetchLastItems, fetchUserShows } from './data' import * as c from '../constants' import * as selectors from '../selectors' +import { get } from 'lodash' import { push } from 'react-router-redux' export function setPlaylist(playlist) { @@ -12,7 +13,8 @@ export function setPlaylist(playlist) { export function initQueueList() { return (dispatch, getState) => { - const currentUserId = getState().auth.user.id + const currentUserId = get(getState().auth, 'user.id') + if (!initQueueList) {return} return Promise.all([ // LAST ITEMS fetchLastItems({ user: currentUserId }) From 992108c9656b2ad4aad95c1847720422eafd8d4c Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Wed, 12 Apr 2017 16:49:48 +0200 Subject: [PATCH 011/143] Docker :boom: --- .gitignore | 1 + Dockerfile | 24 ------------- app/l8pr/static/actions/player.js | 2 +- app/l8pr/static/selectors.js | 2 ++ app/settings.py | 5 --- app/settings_docker.py | 7 ++-- docker-common.yml | 27 ++++++++++++++ docker-compose.yml | 58 +++++++++++++++++++----------- docker/django/Dockerfile | 12 +++++++ docker/django/django-entrypoint.sh | 14 ++++++++ docker/nginx/default.conf | 13 +++++++ docker/postgres/init-user-db.sh | 9 +++++ docker/web/Dockerfile | 14 ++++++++ docker/web/web-entrypoint.sh | 4 +++ 14 files changed, 138 insertions(+), 54 deletions(-) delete mode 100644 Dockerfile create mode 100644 docker-common.yml create mode 100644 docker/django/Dockerfile create mode 100755 docker/django/django-entrypoint.sh create mode 100644 docker/nginx/default.conf create mode 100755 docker/postgres/init-user-db.sh create mode 100644 docker/web/Dockerfile create mode 100755 docker/web/web-entrypoint.sh diff --git a/.gitignore b/.gitignore index a61e4da..ff9a939 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ app/l8pr/static/bower_components staticfiles/ uploaded/ static_dist/ +yarn.lock diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 5de435f..0000000 --- a/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM python:3.4.3 -ENV PYTHONUNBUFFERED 1 -RUN mkdir /code -WORKDIR /code - -RUN apt-get update || true -RUN curl -sL https://deb.nodesource.com/setup_4.x | bash - -RUN apt-get install -y nodejs -RUN apt-get install -y openjdk-7-jdk nodejs chromium Xvfb - -ADD requirements.txt /code/ -RUN pip install -r requirements.txt - -COPY ./package.json /code/ -COPY ./bower.json /code/ -COPY ./.bowerrc /code/ - -RUN npm install -g npm -RUN npm install -g protractor -RUN webdriver-manager update -RUN npm install -g bower -RUN npm install -RUN bower --allow-root install -ADD . /code/ diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index d13a901..362b251 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -13,7 +13,7 @@ export function setPlaylist(playlist) { export function initQueueList() { return (dispatch, getState) => { - const currentUserId = get(getState().auth, 'user.id') + const currentUserId = selectors.currentUserId(getState()) if (!initQueueList) {return} return Promise.all([ // LAST ITEMS diff --git a/app/l8pr/static/selectors.js b/app/l8pr/static/selectors.js index 2a7604e..4d68989 100644 --- a/app/l8pr/static/selectors.js +++ b/app/l8pr/static/selectors.js @@ -5,6 +5,8 @@ export const currentTrack = (state) => state.player.current export const currentShow = (state) => get(state, 'player.current.context') export const playlist = (state) => state.player.playlist export const getPathname = (state) => state.routing.locationBeforeTransitions.pathname +export const currentUser = (state) => get(state.auth, 'user') +export const currentUserId = createSelector(currentUser, (user) => get(user, 'id')) export const getLocation = createSelector( [getPathname], diff --git a/app/settings.py b/app/settings.py index d55bc59..b26a72b 100644 --- a/app/settings.py +++ b/app/settings.py @@ -37,13 +37,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'webpack_loader', - # 'compressor', 'rest_framework', 'rest_framework.authtoken', - 'oauth2_provider', - # 'social.apps.django_app.default', - # 'rest_framework_social_oauth2', 'djoser', 'haystack', 'drf_haystack', diff --git a/app/settings_docker.py b/app/settings_docker.py index f78d337..125175c 100644 --- a/app/settings_docker.py +++ b/app/settings_docker.py @@ -3,9 +3,10 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'postgres', - 'USER': 'postgres', - 'HOST': 'db', + 'NAME': 'loopr_dev', + 'USER': 'loopr', + 'PASSWORD': 'password', + 'HOST': 'postgres', 'PORT': 5432, } } diff --git a/docker-common.yml b/docker-common.yml new file mode 100644 index 0000000..a165492 --- /dev/null +++ b/docker-common.yml @@ -0,0 +1,27 @@ +version: '2' + +services: + nginx: + restart: always + image: nginx:1.11.6-alpine + postgres: + restart: always + image: postgres:9.5.4 + expose: + - 5432 + volumes: + - ./docker/postgres/data:/var/lib/postgresql + django: + restart: always + build: + context: . + dockerfile: ./docker/django/Dockerfile + volumes: + - .:/django + web: + restart: always + build: + context: . + dockerfile: ./docker/web/Dockerfile + volumes: + - .:/django diff --git a/docker-compose.yml b/docker-compose.yml index f9ba42a..8ccc37b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,39 @@ version: '2' + services: - db: - image: postgres - volumes: - - ./data/postgres:/var/lib/postgresql/data - elasticsearch: - volumes: - - ./data/elastic:/usr/share/elasticsearch/data - image: elasticsearch - web: - build: . - env_file: .env - command: bash -c "sleep 5; python manage.py runserver 0.0.0.0:8000" - volumes: - - .:/code - ports: - - "8000:8000" - environment: - - DJANGO_SETTINGS_MODULE=app.settings_docker - depends_on: - - elasticsearch - - db + nginx: + extends: + file: docker-common.yml + service: nginx + ports: + - 8000:8000 + volumes: + - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf + volumes_from: + - backend + postgres: + extends: + file: docker-common.yml + service: postgres + ports: + - 5433:5432 + volumes: + - ./docker/postgres/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh + backend: + extends: + file: docker-common.yml + service: django + links: + - postgres + entrypoint: + - /django-entrypoint.sh + expose: + - 8000 + frontend: + extends: + file: docker-common.yml + service: web + links: + - backend + entrypoint: + - /web-entrypoint.sh diff --git a/docker/django/Dockerfile b/docker/django/Dockerfile new file mode 100644 index 0000000..63efb94 --- /dev/null +++ b/docker/django/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.4.3 +MAINTAINER Filipe Garcia + +ENV PYTHONUNBUFFERED 1 + +COPY ./docker/django/django-entrypoint.sh / +COPY ./requirements.txt /django/requirements.txt + +WORKDIR /django + +RUN pip install pip==9.0.1 +RUN pip install -r requirements.txt diff --git a/docker/django/django-entrypoint.sh b/docker/django/django-entrypoint.sh new file mode 100755 index 0000000..3290417 --- /dev/null +++ b/docker/django/django-entrypoint.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +until cd app +do + echo "Waiting for django volume..." +done + +until python ../manage.py migrate --settings=app.settings_docker +do + echo "Waiting for postgres ready..." + sleep 2 +done + +python ../manage.py runserver 0.0.0.0:8000 --settings=app.settings_docker diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..c75136a --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,13 @@ +server { + # Set the port to listen on and the server name + listen 8000 default_server; + + client_max_body_size 20M; + + location / { + proxy_set_header Host $http_host; # django uses this by default + proxy_set_header X-Forwarded-Host $server_name; # also in django settings (could disable) + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://backend:8000; + } +} diff --git a/docker/postgres/init-user-db.sh b/docker/postgres/init-user-db.sh new file mode 100755 index 0000000..d3411eb --- /dev/null +++ b/docker/postgres/init-user-db.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + CREATE USER loopr WITH PASSWORD 'password' CREATEDB; + CREATE DATABASE loopr_dev; + GRANT ALL PRIVILEGES ON DATABASE loopr_dev TO loopr; +EOSQL diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile new file mode 100644 index 0000000..a4cd48d --- /dev/null +++ b/docker/web/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:14.04 +MAINTAINER Filipe Garcia + +COPY ./docker/web/web-entrypoint.sh / +COPY ./package.json /django/package.json + +WORKDIR /django + +RUN apt-get update && apt-get install -y curl +RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +RUN echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list + +RUN apt-get update && apt-get install -y nodejs yarn diff --git a/docker/web/web-entrypoint.sh b/docker/web/web-entrypoint.sh new file mode 100755 index 0000000..4e31e2c --- /dev/null +++ b/docker/web/web-entrypoint.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +yarn +npm run dev From 71333471cd8edf89040820ebb05305dedb3e75c2 Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Wed, 12 Apr 2017 17:53:44 +0200 Subject: [PATCH 012/143] hotkey toggle strip --- app/l8pr/static/index.js | 13 +++++++++++-- package.json | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/l8pr/static/index.js b/app/l8pr/static/index.js index 77fcc46..6ab8454 100644 --- a/app/l8pr/static/index.js +++ b/app/l8pr/static/index.js @@ -2,10 +2,11 @@ import React from 'react' import ReactDOM from 'react-dom' import { browserHistory } from 'react-router' import { syncHistoryWithStore } from 'react-router-redux' - +import { HotKeys } from 'react-hotkeys' import Root from './containers/Root/Root' import configureStore from './store/configureStore' import { authLoginUserSuccess } from './actions/auth' +import * as browser from './actions/browser' const initialState = {} @@ -14,8 +15,16 @@ const target = document.getElementById('root') const store = configureStore(initialState, browserHistory) const history = syncHistoryWithStore(browserHistory, store) +const handlers = { + toggleStrip: () => store.dispatch(browser.toggleStrip()), +} +const map = { + toggleStrip: 'c', +} const node = ( - + + + ) const token = localStorage.getItem('token') diff --git a/package.json b/package.json index d66cb88..2c2c3d7 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "react": "15.4.1", "react-bootstrap": "^0.30.8", "react-dom": "15.4.1", + "react-hotkeys": "^0.9.0", "react-mixin": "3.0.5", "react-player": "^0.14.3", "react-redux": "4.4.6", From 661f9176d78d0e93c6585d81995a6d43c4d4da3f Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Wed, 12 Apr 2017 18:44:02 +0200 Subject: [PATCH 013/143] toggle playpause with space --- app/l8pr/static/actions/player.js | 10 ++++++++++ app/l8pr/static/containers/Home/style.scss | 1 + app/l8pr/static/index.js | 9 ++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index 362b251..298336a 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -73,6 +73,16 @@ export function play() { } } +export function togglePlay() { + return (dispatch, getState) => { + if (getState().player.playing) { + dispatch(pause()) + } else { + dispatch(play()) + } + } +} + export function previousContext() { return { type: c.PREVIOUS_CONTEXT } } diff --git a/app/l8pr/static/containers/Home/style.scss b/app/l8pr/static/containers/Home/style.scss index 7192bba..0955645 100644 --- a/app/l8pr/static/containers/Home/style.scss +++ b/app/l8pr/static/containers/Home/style.scss @@ -5,4 +5,5 @@ top: 0; bottom: 0; font-family: 'Montserrat', 'Trebuchet Ms', sans-serif, "Helvetica Neue", Helvetica, Arial, sans-serif; + overflow: hidden; } diff --git a/app/l8pr/static/index.js b/app/l8pr/static/index.js index 6ab8454..322e433 100644 --- a/app/l8pr/static/index.js +++ b/app/l8pr/static/index.js @@ -7,6 +7,7 @@ import Root from './containers/Root/Root' import configureStore from './store/configureStore' import { authLoginUserSuccess } from './actions/auth' import * as browser from './actions/browser' +import * as player from './actions/player' const initialState = {} @@ -15,11 +16,13 @@ const target = document.getElementById('root') const store = configureStore(initialState, browserHistory) const history = syncHistoryWithStore(browserHistory, store) -const handlers = { - toggleStrip: () => store.dispatch(browser.toggleStrip()), -} const map = { toggleStrip: 'c', + playPause: 'space', +} +const handlers = { + toggleStrip: () => store.dispatch(browser.toggleStrip()), + playPause: () => store.dispatch(player.togglePlay()), } const node = ( From d624bc1a39d3840dabd6e9b0fb4baf0ec98b68c6 Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Wed, 12 Apr 2017 18:44:40 +0200 Subject: [PATCH 014/143] settings basic --- app/settings.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/settings.py b/app/settings.py index b26a72b..e17b3c1 100644 --- a/app/settings.py +++ b/app/settings.py @@ -83,17 +83,9 @@ # https://docs.djangoproject.com/en/1.9/ref/settings/#databases DATABASES = { - # 'default': { - # 'ENGINE': 'django.db.backends.sqlite3', - # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - # } 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'loopr', - 'USER': 'postgres', - 'PASSWORD': 'mysecretpassword', - 'HOST': '172.17.0.2', - 'PORT': '5432', + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } From f0b625df86466ea06b7f3f9d90589b57acef0c68 Mon Sep 17 00:00:00 2001 From: Benetco Date: Wed, 12 Apr 2017 20:32:28 +0200 Subject: [PATCH 015/143] html/css tuning - items and Shows in QueueList --- app/l8pr/static/components/ListItem/index.js | 6 ++--- .../static/components/ListItem/style.scss | 24 +++++++++++++++++-- app/l8pr/static/components/PlayQueue/index.js | 11 ++++++--- .../static/components/PlayQueue/style.scss | 22 +++++++++++++++-- app/l8pr/static/styles/config/_variables.scss | 9 ++++++- 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/app/l8pr/static/components/ListItem/index.js b/app/l8pr/static/components/ListItem/index.js index 9f02c8b..c241e9c 100644 --- a/app/l8pr/static/components/ListItem/index.js +++ b/app/l8pr/static/components/ListItem/index.js @@ -7,11 +7,11 @@ export default function ListItem({ item, onPlayClick }) {
{item.title} - {moment.duration(item.duration, 's').humanize()} -
{item.description}
+ {moment.duration(item.duration, 's').humanize()} +
{item.description}
- +
) diff --git a/app/l8pr/static/components/ListItem/style.scss b/app/l8pr/static/components/ListItem/style.scss index 0cb28e7..431c200 100644 --- a/app/l8pr/static/components/ListItem/style.scss +++ b/app/l8pr/static/components/ListItem/style.scss @@ -1,4 +1,24 @@ +.ListItem { + font-family: $primaryFont; + background-color: white; + border-bottom: 1px solid $colorItem; + padding: 5px 25px; + color: $colorItem; +} + +.ListItem__title { + display: inline-block; + +} + +.ListItem__details { + display: inline-block; +} + +.ListItem__desc { + max-height: 50px; + overflow: scroll; +} + .ListItem__thumbnail { - width: 50px; - height: 50px; } diff --git a/app/l8pr/static/components/PlayQueue/index.js b/app/l8pr/static/components/PlayQueue/index.js index 028821a..b95a9ea 100644 --- a/app/l8pr/static/components/PlayQueue/index.js +++ b/app/l8pr/static/components/PlayQueue/index.js @@ -16,9 +16,14 @@ export default class PlayQueue extends React.Component {
    {contexts.map((c) => ( -
  • -
    {c.context.title}
    -
    {moment.duration(getDuration(c.items), 's').humanize()}
    +
  • +
    +
    {c.context.title}
    +
    + 66 items / + {moment.duration(getDuration(c.items), 's').humanize()} +
    +
      {c.items.map(i => (
    1. diff --git a/app/l8pr/static/components/PlayQueue/style.scss b/app/l8pr/static/components/PlayQueue/style.scss index 12f056b..5a9cba2 100644 --- a/app/l8pr/static/components/PlayQueue/style.scss +++ b/app/l8pr/static/components/PlayQueue/style.scss @@ -1,8 +1,26 @@ .PlayQueue { - max-height: 74vh; + max-height: 55vh; overflow: auto; +} + +.context { + border-bottom: 10px solid $colorShow; + background-image: url('https://i.giphy.com/yidUziP8gSfROqx0Zy.gif'); +} + +.context_cover { + vertical-align: top; + display: inline-block; + padding: 15px 25px; background-color: white; + color: $colorShow; } + .context__title { - font-size: 1.25em; + font-family: $secondaryFont; + font-size: 2em; +} + +.context__details { + font-family: $primaryFont; } diff --git a/app/l8pr/static/styles/config/_variables.scss b/app/l8pr/static/styles/config/_variables.scss index 688f25f..8cf4766 100644 --- a/app/l8pr/static/styles/config/_variables.scss +++ b/app/l8pr/static/styles/config/_variables.scss @@ -7,6 +7,13 @@ $bootstrap-sass-asset-helper: false !default; // Variables // -------------------------------------------------- +$colorPlaying: magenta; +$colorItem: blue; +$colorShow: rgba(0,250,150,1); +$colorFeed: yellow; + +$primaryFont: 'Source Code Pro'; +$secondaryFont: 'Montserrat'; //== Colors // @@ -53,7 +60,7 @@ $font-family-serif: Georgia, "Times New Roman", Times, serif !default; $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace !default; $font-family-base: $font-family-sans-serif !default; -$font-size-base: 14px !default; +$font-size-base: 11px !default; $font-size-large: ceil(($font-size-base * 1.25)) !default; // ~18px $font-size-small: ceil(($font-size-base * 0.85)) !default; // ~12px From 53c3ecc822636bdbf59c3daf95c1c43b5386f5d4 Mon Sep 17 00:00:00 2001 From: Benetco Date: Wed, 12 Apr 2017 21:26:38 +0200 Subject: [PATCH 016/143] html/css - items and Shows in Queuelist - 2 --- app/l8pr/static/components/NavPlayer/style.scss | 8 +++++++- app/l8pr/static/containers/StripHeader/index.js | 8 ++++---- .../static/containers/StripHeader/style.scss | 16 +++++++++++++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/l8pr/static/components/NavPlayer/style.scss b/app/l8pr/static/components/NavPlayer/style.scss index 5553764..4b032be 100644 --- a/app/l8pr/static/components/NavPlayer/style.scss +++ b/app/l8pr/static/components/NavPlayer/style.scss @@ -1,3 +1,9 @@ .NavPlayer { - background-color: rgba(255, 255, 255, 0.85); + background-color: $colorPlaying; + padding: 10px; + color: white; +} + +.NavPlayer a { + color: white; } diff --git a/app/l8pr/static/containers/StripHeader/index.js b/app/l8pr/static/containers/StripHeader/index.js index d801a45..1f06f0b 100644 --- a/app/l8pr/static/containers/StripHeader/index.js +++ b/app/l8pr/static/containers/StripHeader/index.js @@ -9,10 +9,10 @@ import './style.scss' function StripHeaderComponent({ trackTitle, stripOpened, toggleStrip, showTitle, onLogin }) { return (
      - - {trackTitle}
      - {showTitle} -
      +
      +
      {trackTitle}

      +
      {showTitle}
      +
      {!stripOpened && 'keyboard_arrow_up'} diff --git a/app/l8pr/static/containers/StripHeader/style.scss b/app/l8pr/static/containers/StripHeader/style.scss index d4a8d35..296f786 100644 --- a/app/l8pr/static/containers/StripHeader/style.scss +++ b/app/l8pr/static/containers/StripHeader/style.scss @@ -1,7 +1,8 @@ .StripHeader { background-color: white; + padding: 10px; } -.StripHeader__title { +.StripHeader__playingNow { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -10,3 +11,16 @@ font-weight: 500; text-align: left; } + +.StripHeader__playingNow__item { + color: $colorPlaying; + font-family: $primaryFont; + font-size: 2em; + display: inline-block; +} + +.StripHeader__playingNow__show { + font-family: $secondaryFont; + font-size: 1em; + display: inline-block; +} From 5d3241679e438262d3b6a41912c93ca4c8f330dd Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Thu, 13 Apr 2017 10:15:15 +0200 Subject: [PATCH 017/143] remove mockup from PlayQueue --- app/l8pr/static/components/PlayQueue/index.js | 15 +++++++++----- .../static/components/PlayQueue/style.scss | 20 ++++++++++++++++--- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/l8pr/static/components/PlayQueue/index.js b/app/l8pr/static/components/PlayQueue/index.js index b95a9ea..5424a80 100644 --- a/app/l8pr/static/components/PlayQueue/index.js +++ b/app/l8pr/static/components/PlayQueue/index.js @@ -17,11 +17,16 @@ export default class PlayQueue extends React.Component {
        {contexts.map((c) => (
      • -
        -
        {c.context.title}
        -
        - 66 items / - {moment.duration(getDuration(c.items), 's').humanize()} +
        +
        + {c.context.title}
        + {c.items.length} items / + {moment.duration(getDuration(c.items), 's').humanize()} +
        +
        + {c.items.slice(0, 15).map((i)=> ( + + ))}
          diff --git a/app/l8pr/static/components/PlayQueue/style.scss b/app/l8pr/static/components/PlayQueue/style.scss index 5a9cba2..fb1dadb 100644 --- a/app/l8pr/static/components/PlayQueue/style.scss +++ b/app/l8pr/static/components/PlayQueue/style.scss @@ -5,22 +5,36 @@ .context { border-bottom: 10px solid $colorShow; - background-image: url('https://i.giphy.com/yidUziP8gSfROqx0Zy.gif'); } - -.context_cover { +.context__cover { vertical-align: top; display: inline-block; padding: 15px 25px; background-color: white; color: $colorShow; + display: flex; +} + +.context__illustrations { + padding-left: 20px; + white-space: nowrap; + overflow: hidden; + max-height: 100px; + width: 100%; + img { + background-size: cover; + height: 100%; + } } .context__title { font-family: $secondaryFont; font-size: 2em; + flex-grow: 1; } .context__details { font-family: $primaryFont; + font-size: 0.5em; + } From 1c7d5c8a56008bd266399dd03a615d08e22bebe4 Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Thu, 13 Apr 2017 10:40:34 +0200 Subject: [PATCH 018/143] playqueue style --- app/l8pr/static/actions/data.js | 8 ++++---- app/l8pr/static/actions/player.js | 11 ++++++----- app/l8pr/static/components/PlayQueue/style.scss | 5 ++++- app/l8pr/static/containers/Home/index.js | 8 +------- app/l8pr/static/index.js | 13 +++++++++---- app/l8pr/static/selectors.js | 15 +++++++++------ 6 files changed, 33 insertions(+), 27 deletions(-) diff --git a/app/l8pr/static/actions/data.js b/app/l8pr/static/actions/data.js index 4fd0370..118783e 100644 --- a/app/l8pr/static/actions/data.js +++ b/app/l8pr/static/actions/data.js @@ -6,8 +6,8 @@ import { checkHttpStatus, parseJSON } from '../utils' import * as c from '../constants' import { authLoginUserFailure } from './auth' -export function fetchLastItems({ user }) { - return fetch(`${SERVER_URL}/api/items/?users=${user}&limit=10`, { +export function fetchLastItems({ username }) { + return fetch(`${SERVER_URL}/api/items/?users=${username}&limit=10`, { // credentials: 'include', headers: { Accept: 'application/json', @@ -19,8 +19,8 @@ export function fetchLastItems({ user }) { .then((data) => data.results) } -export function fetchUserShows({ user }) { - return fetch(`${SERVER_URL}/api/loops/?user=${user}`, { +export function fetchUserShows({ username }) { + return fetch(`${SERVER_URL}/api/loops/?username=${username}`, { // credentials: 'include', headers: { Accept: 'application/json', diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index 298336a..76cb9d3 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -1,7 +1,6 @@ import { fetchLastItems, fetchUserShows } from './data' import * as c from '../constants' import * as selectors from '../selectors' -import { get } from 'lodash' import { push } from 'react-router-redux' export function setPlaylist(playlist) { @@ -11,20 +10,22 @@ export function setPlaylist(playlist) { } } -export function initQueueList() { +export function initQueueList({ user, queue, item }) { return (dispatch, getState) => { - const currentUserId = selectors.currentUserId(getState()) + console.log(user, selectors.currentUser(getState())) + const username = user && user || selectors.currentUser(getState()).username + console.log(username) if (!initQueueList) {return} return Promise.all([ // LAST ITEMS - fetchLastItems({ user: currentUserId }) + fetchLastItems({ username: username }) // set context .then((items) => (items.map((i) => ({ ...i, context: { title: 'Last Items' }, })))), // SHOWS - fetchUserShows({ user: currentUserId }) + fetchUserShows({ username: username }) .then((shows) => ( shows.map((show) => ( // set context diff --git a/app/l8pr/static/components/PlayQueue/style.scss b/app/l8pr/static/components/PlayQueue/style.scss index fb1dadb..4e4302f 100644 --- a/app/l8pr/static/components/PlayQueue/style.scss +++ b/app/l8pr/static/components/PlayQueue/style.scss @@ -20,7 +20,8 @@ white-space: nowrap; overflow: hidden; max-height: 100px; - width: 100%; + flex-shrink: 1; + text-align: right; img { background-size: cover; height: 100%; @@ -31,10 +32,12 @@ font-family: $secondaryFont; font-size: 2em; flex-grow: 1; + width: auto; } .context__details { font-family: $primaryFont; font-size: 0.5em; + white-space: nowrap; } diff --git a/app/l8pr/static/containers/Home/index.js b/app/l8pr/static/containers/Home/index.js index 4ab9c1c..488212a 100644 --- a/app/l8pr/static/containers/Home/index.js +++ b/app/l8pr/static/containers/Home/index.js @@ -2,23 +2,18 @@ import React from 'react' import { connect } from 'react-redux' import { Screen } from '../../components' import { Strip, ModalsContainer } from '../index' -import { play, pause, next, initQueueList } from '../../actions/player' +import { play, pause, next } from '../../actions/player' import * as selectors from '../../selectors' import './style.scss' class HomeView extends React.Component { - componentWillMount() { - this.props.initQueueList() - } - static propTypes = { media: React.PropTypes.object, playing: React.PropTypes.bool, nextItem: React.PropTypes.func, play: React.PropTypes.func, pause: React.PropTypes.func, - initQueueList: React.PropTypes.func, volume: React.PropTypes.number, } @@ -52,7 +47,6 @@ const mapDispatchToProps = (dispatch) => ({ nextItem: () => (dispatch(next())), play: (args) => (dispatch(play(args))), pause: (args) => (dispatch(pause(args))), - initQueueList: () => (dispatch(initQueueList())), }) export default connect(mapStateToProps, mapDispatchToProps)(HomeView) diff --git a/app/l8pr/static/index.js b/app/l8pr/static/index.js index 322e433..aedd71a 100644 --- a/app/l8pr/static/index.js +++ b/app/l8pr/static/index.js @@ -8,7 +8,7 @@ import configureStore from './store/configureStore' import { authLoginUserSuccess } from './actions/auth' import * as browser from './actions/browser' import * as player from './actions/player' - +import * as selectors from './selectors' const initialState = {} const target = document.getElementById('root') @@ -31,15 +31,20 @@ const node = ( ) const token = localStorage.getItem('token') -let user = {} +let currentUser = {} try { - user = JSON.parse(localStorage.getItem('user')) + currentUser = JSON.parse(localStorage.getItem('user')) } catch (e) { // Failed to parse } if (token !== null) { - store.dispatch(authLoginUserSuccess(token, user)) + store.dispatch(authLoginUserSuccess(token, currentUser)) } +store.dispatch(player.initQueueList( + selectors.getLocation(store.getState()) +)) + + ReactDOM.render(node, target) diff --git a/app/l8pr/static/selectors.js b/app/l8pr/static/selectors.js index 4d68989..80492d4 100644 --- a/app/l8pr/static/selectors.js +++ b/app/l8pr/static/selectors.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect' -import { get } from 'lodash' +import { get, zipObject } from 'lodash' export const currentTrack = (state) => state.player.current export const currentShow = (state) => get(state, 'player.current.context') @@ -11,11 +11,14 @@ export const currentUserId = createSelector(currentUser, (user) => get(user, 'id export const getLocation = createSelector( [getPathname], (pathname) => { - const locationRegex = /show\/([0-9]+)\/item\/([0-9]+)/g - const m = locationRegex.exec(pathname) - return { - show: get(m, '[1]'), - item: get(m, '[2]'), + function getValueForKey(key) { + const regex = `/${key}/([a-z0-9]+)` + const re = RegExp(regex, 'gi') + const m = re.exec(pathname) + return get(m, '[1]') } + const keys = ['user', 'item', 'show'] + const values = keys.map((key) => getValueForKey(key)) + return zipObject(keys, values) } ) From 5414aa718a009094f680f45c4f4e0fa0ef66969f Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Thu, 13 Apr 2017 10:41:33 +0200 Subject: [PATCH 019/143] remove console log --- app/l8pr/static/actions/player.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index 76cb9d3..adab50b 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -12,9 +12,7 @@ export function setPlaylist(playlist) { export function initQueueList({ user, queue, item }) { return (dispatch, getState) => { - console.log(user, selectors.currentUser(getState())) const username = user && user || selectors.currentUser(getState()).username - console.log(username) if (!initQueueList) {return} return Promise.all([ // LAST ITEMS From 7e5d40d5da2eeef9c51277a3970b20d33291688a Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Thu, 13 Apr 2017 14:43:59 +0200 Subject: [PATCH 020/143] config soundcloud --- app/l8pr/static/actions/data.js | 50 ----------- app/l8pr/static/actions/player.js | 7 ++ app/l8pr/static/components/ListItem/index.js | 6 +- .../static/components/ListItem/style.scss | 8 -- app/l8pr/static/components/PlayQueue/index.js | 29 +------ .../static/components/PlayQueue/style.scss | 2 +- app/l8pr/static/components/Screen/index.js | 4 +- app/l8pr/static/containers/Home/index.js | 2 + app/l8pr/static/containers/Protected/index.js | 83 ------------------- app/l8pr/static/containers/Strip/index.js | 5 +- app/l8pr/static/containers/index.js | 1 - app/l8pr/static/index.js | 2 + app/l8pr/static/reducers/player.js | 4 + app/l8pr/static/routes.js | 3 +- app/l8pr/static/selectors.js | 32 ++++++- app/l8pr/static/utils/config.js | 3 +- docker-common.yml | 6 ++ docker-compose.yml | 5 ++ webpack/common.config.js | 6 +- 19 files changed, 76 insertions(+), 182 deletions(-) delete mode 100644 app/l8pr/static/containers/Protected/index.js diff --git a/app/l8pr/static/actions/data.js b/app/l8pr/static/actions/data.js index 118783e..1a25e9c 100644 --- a/app/l8pr/static/actions/data.js +++ b/app/l8pr/static/actions/data.js @@ -1,10 +1,7 @@ import fetch from 'isomorphic-fetch' -import { push } from 'react-router-redux' import { SERVER_URL } from '../utils/config' import { checkHttpStatus, parseJSON } from '../utils' -import * as c from '../constants' -import { authLoginUserFailure } from './auth' export function fetchLastItems({ username }) { return fetch(`${SERVER_URL}/api/items/?users=${username}&limit=10`, { @@ -31,50 +28,3 @@ export function fetchUserShows({ username }) { .then(parseJSON) .then((items) => items[0].shows_list) } - -export function dataReceiveProtectedData(data) { - return { - type: c.DATA_RECEIVE_PROTECTED_DATA, - payload: { data }, - } -} - -export function dataFetchProtectedDataRequest() { - return { type: c.DATA_FETCH_PROTECTED_DATA_REQUEST } -} - -export function dataFetchProtectedData(token) { - return (dispatch) => { - dispatch(dataFetchProtectedDataRequest()) - return fetch(`${SERVER_URL}/api/v1/getdata/`, { - credentials: 'include', - headers: { - Accept: 'application/json', - Authorization: `Token ${token}`, - }, - }) - .then(checkHttpStatus) - .then(parseJSON) - .then((response) => { - dispatch(dataReceiveProtectedData(response.data)) - }) - .catch((error) => { - if (error && typeof error.response !== 'undefined' && error.response.status === 401) { - // Invalid authentication credentials - return error.response.json().then((data) => { - dispatch(authLoginUserFailure(401, data.non_field_errors[0])) - dispatch(push('/login')) - }) - } else if (error && typeof error.response !== 'undefined' && error.response.status >= 500) { - // Server side error - dispatch(authLoginUserFailure(500, 'A server error occurred while sending your data!')) - } else { - // Most likely connection issues - dispatch(authLoginUserFailure('Connection Error', 'An error occurred while sending your data!')) - } - - dispatch(push('/login')) - return Promise.resolve() // TODO: we need a promise here because of the tests, find a better way - }) - } -} diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index adab50b..591d909 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -10,6 +10,10 @@ export function setPlaylist(playlist) { } } +export function fillQueueList() { + +} + export function initQueueList({ user, queue, item }) { return (dispatch, getState) => { const username = user && user || selectors.currentUser(getState()).username @@ -69,6 +73,9 @@ export function play() { url += `/item/${item.id}` } dispatch(push(url)) + // if (selectors.getPlaylistGroupedByContext(getState()).length < 3) { + // dispatch(fillQueueList()) + // } } } diff --git a/app/l8pr/static/components/ListItem/index.js b/app/l8pr/static/components/ListItem/index.js index c241e9c..96513d9 100644 --- a/app/l8pr/static/components/ListItem/index.js +++ b/app/l8pr/static/components/ListItem/index.js @@ -7,11 +7,7 @@ export default function ListItem({ item, onPlayClick }) {
          {item.title} - {moment.duration(item.duration, 's').humanize()} -
          {item.description}
          -
          -
          - +  {moment.duration(item.duration, 's').humanize()}
          ) diff --git a/app/l8pr/static/components/ListItem/style.scss b/app/l8pr/static/components/ListItem/style.scss index 431c200..305bfe5 100644 --- a/app/l8pr/static/components/ListItem/style.scss +++ b/app/l8pr/static/components/ListItem/style.scss @@ -14,11 +14,3 @@ .ListItem__details { display: inline-block; } - -.ListItem__desc { - max-height: 50px; - overflow: scroll; -} - -.ListItem__thumbnail { -} diff --git a/app/l8pr/static/components/PlayQueue/index.js b/app/l8pr/static/components/PlayQueue/index.js index 5424a80..0ab9b24 100644 --- a/app/l8pr/static/components/PlayQueue/index.js +++ b/app/l8pr/static/components/PlayQueue/index.js @@ -1,6 +1,5 @@ import React from 'react' import { ListItem } from '../index' -import { get } from 'lodash' import moment from 'moment' import './style.scss' @@ -10,8 +9,7 @@ export default class PlayQueue extends React.Component { super(props) } render() { - const { items, onItemPlayClick } = this.props - const contexts = groupByContext(items) + const { contexts, onItemPlayClick } = this.props return (
            @@ -24,8 +22,8 @@ export default class PlayQueue extends React.Component { {moment.duration(getDuration(c.items), 's').humanize()}
          - {c.items.slice(0, 15).map((i)=> ( - + {c.items.slice(0, 15).map((i, idx)=> ( + ))}
        @@ -45,29 +43,10 @@ export default class PlayQueue extends React.Component { } PlayQueue.propTypes = { - items: React.PropTypes.array.isRequired, + contexts: React.PropTypes.array.isRequired, onItemPlayClick: React.PropTypes.func.isRequired, } -function groupByContext(items) { - const contexts = [{ - context: items[0].context, - items: [], - }] - items.forEach((i) => { - const lastContext = contexts[contexts.length - 1] - if (i.context.id !== get(lastContext, 'context.id')) { - contexts.push({ - context: null, - items: [], - }) - } - if (contexts[contexts.length - 1].context === null) contexts[contexts.length - 1].context = i.context - contexts[contexts.length - 1].items.push(i) - }) - return contexts -} - function getDuration(items) { return items.reduce((r, i) => (r + i.duration), 0) } diff --git a/app/l8pr/static/components/PlayQueue/style.scss b/app/l8pr/static/components/PlayQueue/style.scss index 4e4302f..c8b88e5 100644 --- a/app/l8pr/static/components/PlayQueue/style.scss +++ b/app/l8pr/static/components/PlayQueue/style.scss @@ -20,7 +20,7 @@ white-space: nowrap; overflow: hidden; max-height: 100px; - flex-shrink: 1; + flex-shrink: 3; text-align: right; img { background-size: cover; diff --git a/app/l8pr/static/components/Screen/index.js b/app/l8pr/static/components/Screen/index.js index 15c5bac..720a253 100644 --- a/app/l8pr/static/components/Screen/index.js +++ b/app/l8pr/static/components/Screen/index.js @@ -1,6 +1,6 @@ -import React from 'react'; +import React from 'react' import ReactPlayer from 'react-player' -import './style.scss'; +import './style.scss' export default class Screen extends React.Component { diff --git a/app/l8pr/static/containers/Home/index.js b/app/l8pr/static/containers/Home/index.js index 488212a..a734e15 100644 --- a/app/l8pr/static/containers/Home/index.js +++ b/app/l8pr/static/containers/Home/index.js @@ -3,6 +3,7 @@ import { connect } from 'react-redux' import { Screen } from '../../components' import { Strip, ModalsContainer } from '../index' import { play, pause, next } from '../../actions/player' +import { SOUNDCLOUD_API } from '../../utils/config' import * as selectors from '../../selectors' import './style.scss' @@ -24,6 +25,7 @@ class HomeView extends React.Component { {this.props.media && -
        -

        Protected

        - {this.props.isFetching === true ? -

        Loading data...

        - : -
        -

        Data received from the server:

        -
        -
        - {this.props.data} -
        -
        -
        -
        How does this work?
        -

        - On the componentWillMount method of the -  ProtectedView component, the action -  dataFetchProtectedData is called. This action will first - dispatch a DATA_FETCH_PROTECTED_DATA_REQUEST action to the Redux - store. When an action is dispatched to the store, an appropriate reducer for - that specific action will change the state of the store. After that it will then - make an asynchronous request to the server using - the isomorphic-fetch library. On its - response, it will dispatch the DATA_RECEIVE_PROTECTED_DATA action - to the Redux store. In case of wrong credentials in the request, the  - AUTH_LOGIN_USER_FAILURE action will be dispatched. -

        -

        - Because the ProtectedView is connected to the Redux store, when the - value of a property connected to the view is changed, the view is re-rendered - with the new data. -

        -
        -
        - } -
        -
        - ); - } -} - -const mapStateToProps = (state) => { - return { - data: state.data.data, - isFetching: state.data.isFetching - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - actions: bindActionCreators(actionCreators, dispatch) - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ProtectedView); -export { ProtectedView as ProtectedViewNotConnected }; diff --git a/app/l8pr/static/containers/Strip/index.js b/app/l8pr/static/containers/Strip/index.js index 74ed437..5deea2c 100644 --- a/app/l8pr/static/containers/Strip/index.js +++ b/app/l8pr/static/containers/Strip/index.js @@ -2,6 +2,7 @@ import React from 'react' import { connect } from 'react-redux' import { PlayQueue } from '../../components' import { StripHeader, Controller } from '../index' +import * as selectors from '../../selectors' import * as player from '../../actions/player' import './style.scss' @@ -18,7 +19,7 @@ class Strip extends React.Component {
        { stripOpened && - + }
        @@ -28,7 +29,7 @@ class Strip extends React.Component { const mapStateToProps = (state) => ({ stripOpened: state.browser.stripOpened, - playlist: [state.player.current, ...state.player.playQueue], + playlist: selectors.getPlaylistGroupedByContext(state), }) const mapDispatchToProps = (dispatch) => ({ onItemPlayClick: (item) => (dispatch(player.playItem(item))), diff --git a/app/l8pr/static/containers/index.js b/app/l8pr/static/containers/index.js index 745a20d..8915933 100644 --- a/app/l8pr/static/containers/index.js +++ b/app/l8pr/static/containers/index.js @@ -1,6 +1,5 @@ export HomeView from './Home/index' export LoginView from './Login/index' -export ProtectedView from './Protected/index' export NotFoundView from './NotFound/index' export Strip from './Strip/index' export StripHeader from './StripHeader/index' diff --git a/app/l8pr/static/index.js b/app/l8pr/static/index.js index aedd71a..4906c90 100644 --- a/app/l8pr/static/index.js +++ b/app/l8pr/static/index.js @@ -38,10 +38,12 @@ try { // Failed to parse } +// login from localstorage if (token !== null) { store.dispatch(authLoginUserSuccess(token, currentUser)) } +// init playqueue from url store.dispatch(player.initQueueList( selectors.getLocation(store.getState()) )) diff --git a/app/l8pr/static/reducers/player.js b/app/l8pr/static/reducers/player.js index 493ba04..12d761f 100644 --- a/app/l8pr/static/reducers/player.js +++ b/app/l8pr/static/reducers/player.js @@ -8,6 +8,10 @@ export default function (state = { current: null, playQueue: [], history: [], + collections: { + natural: [], + suggestions: [], + }, muted: false, playing: false, }, action = null) { diff --git a/app/l8pr/static/routes.js b/app/l8pr/static/routes.js index e7cc7d8..e5c3570 100644 --- a/app/l8pr/static/routes.js +++ b/app/l8pr/static/routes.js @@ -1,14 +1,13 @@ import React from 'react' import { Route, IndexRoute } from 'react-router' import App from './app' -import { HomeView, LoginView, ProtectedView, NotFoundView } from './containers' +import { HomeView, LoginView, NotFoundView } from './containers' import requireAuthentication from './utils/requireAuthentication' export default( - {/***/} {/***/} {/***/} diff --git a/app/l8pr/static/selectors.js b/app/l8pr/static/selectors.js index 80492d4..6f15507 100644 --- a/app/l8pr/static/selectors.js +++ b/app/l8pr/static/selectors.js @@ -2,12 +2,18 @@ import { createSelector } from 'reselect' import { get, zipObject } from 'lodash' export const currentTrack = (state) => state.player.current +export const history = (state) => state.player.history export const currentShow = (state) => get(state, 'player.current.context') -export const playlist = (state) => state.player.playlist +export const playQueue = (state) => state.player.playQueue export const getPathname = (state) => state.routing.locationBeforeTransitions.pathname export const currentUser = (state) => get(state.auth, 'user') export const currentUserId = createSelector(currentUser, (user) => get(user, 'id')) +export const playlist = createSelector( + [currentTrack, playQueue], + (currentTrack, playQueue) => ([currentTrack, ...playQueue].filter(i => i !== null)) +) + export const getLocation = createSelector( [getPathname], (pathname) => { @@ -22,3 +28,27 @@ export const getLocation = createSelector( return zipObject(keys, values) } ) + +function groupByContext(items) { + if (items.length < 1) { + return [] + } + const contexts = [{ + context: items[0].context, + items: [], + }] + const getLastContext = () => contexts[contexts.length - 1] + items.forEach((i) => { + if (i.context.id !== get(getLastContext(), 'context.id')) { + contexts.push({ + context: null, + items: [], + }) + } + if (getLastContext().context === null) getLastContext().context = i.context + getLastContext().items.push(i) + }) + return contexts +} + +export const getPlaylistGroupedByContext = createSelector(playlist, (p) => groupByContext(p)) diff --git a/app/l8pr/static/utils/config.js b/app/l8pr/static/utils/config.js index ae5d643..f590375 100644 --- a/app/l8pr/static/utils/config.js +++ b/app/l8pr/static/utils/config.js @@ -1,4 +1,5 @@ -export const SERVER_URL = 'http://localhost:8000'; +export const SERVER_URL = process.env.SERVER_URL || 'http://localhost:8000' +export const SOUNDCLOUD_API = process.env.SOUNDCLOUD_API // config should use named export as there can be different exports, // just need to export default also because of eslint rules diff --git a/docker-common.yml b/docker-common.yml index a165492..39db1a6 100644 --- a/docker-common.yml +++ b/docker-common.yml @@ -4,6 +4,9 @@ services: nginx: restart: always image: nginx:1.11.6-alpine + elasticsearch: + restart: always + image: elasticsearch:2 postgres: restart: always image: postgres:9.5.4 @@ -25,3 +28,6 @@ services: dockerfile: ./docker/web/Dockerfile volumes: - .:/django + environment: + - SOUNDCLOUD_API + - SERVER_URL diff --git a/docker-compose.yml b/docker-compose.yml index 8ccc37b..bfe7285 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,11 +19,16 @@ services: - 5433:5432 volumes: - ./docker/postgres/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh + elasticsearch: + extends: + file: docker-common.yml + service: elasticsearch backend: extends: file: docker-common.yml service: django links: + - elasticsearch - postgres entrypoint: - /django-entrypoint.sh diff --git a/webpack/common.config.js b/webpack/common.config.js index ee7244d..a9cf413 100644 --- a/webpack/common.config.js +++ b/webpack/common.config.js @@ -78,7 +78,11 @@ const common = { inject: 'body' }), new webpack.DefinePlugin({ - 'process.env': { NODE_ENV: TARGET === 'dev' ? '"development"' : '"production"' }, + 'process.env': { + NODE_ENV: TARGET === 'dev' ? '"development"' : '"production"', + SERVER_URL: JSON.stringify(process.env.SERVER_URL), + SOUNDCLOUD_API: JSON.stringify(process.env.SOUNDCLOUD_API), + }, '__DEVELOPMENT__': TARGET === 'dev' }), new webpack.ProvidePlugin({ From 894ed60c450171ffe6866410b31a5cbaf1a4686e Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Thu, 13 Apr 2017 14:59:51 +0200 Subject: [PATCH 021/143] playeQueue moved to containers --- app/l8pr/static/components/ListItem/index.js | 3 ++- app/l8pr/static/components/index.js | 1 - .../PlayQueue/index.js | 25 +++++++++++++++---- .../PlayQueue/style.scss | 0 .../PlayQueue/test.js | 0 app/l8pr/static/containers/Strip/index.js | 17 ++++--------- app/l8pr/static/containers/index.js | 1 + 7 files changed, 28 insertions(+), 19 deletions(-) rename app/l8pr/static/{components => containers}/PlayQueue/index.js (65%) rename app/l8pr/static/{components => containers}/PlayQueue/style.scss (100%) rename app/l8pr/static/{components => containers}/PlayQueue/test.js (100%) diff --git a/app/l8pr/static/components/ListItem/index.js b/app/l8pr/static/components/ListItem/index.js index 96513d9..9a376d2 100644 --- a/app/l8pr/static/components/ListItem/index.js +++ b/app/l8pr/static/components/ListItem/index.js @@ -2,10 +2,11 @@ import React from 'react' import moment from 'moment' import './style.scss' -export default function ListItem({ item, onPlayClick }) { +export default function ListItem({ item, onPlayClick, isPlaying }) { return (
        + {isPlaying && pause} {item.title}  {moment.duration(item.duration, 's').humanize()}
        diff --git a/app/l8pr/static/components/index.js b/app/l8pr/static/components/index.js index c62be82..11cd4d0 100644 --- a/app/l8pr/static/components/index.js +++ b/app/l8pr/static/components/index.js @@ -1,4 +1,3 @@ export Screen from './Screen' -export PlayQueue from './PlayQueue' export ListItem from './ListItem' export LoginModal from './LoginModal' diff --git a/app/l8pr/static/components/PlayQueue/index.js b/app/l8pr/static/containers/PlayQueue/index.js similarity index 65% rename from app/l8pr/static/components/PlayQueue/index.js rename to app/l8pr/static/containers/PlayQueue/index.js index 0ab9b24..a5ce530 100644 --- a/app/l8pr/static/components/PlayQueue/index.js +++ b/app/l8pr/static/containers/PlayQueue/index.js @@ -1,15 +1,17 @@ import React from 'react' -import { ListItem } from '../index' +import { ListItem } from '../../components' import moment from 'moment' - +import { connect } from 'react-redux' +import * as selectors from '../../selectors' import './style.scss' +import * as player from '../../actions/player' -export default class PlayQueue extends React.Component { +class PlayQueue extends React.Component { constructor(props) { super(props) } render() { - const { contexts, onItemPlayClick } = this.props + const { contexts, onItemPlayClick, currentItem } = this.props return (
          @@ -30,7 +32,10 @@ export default class PlayQueue extends React.Component {
            {c.items.map(i => (
          1. - +
          2. ))}
          @@ -45,8 +50,18 @@ export default class PlayQueue extends React.Component { PlayQueue.propTypes = { contexts: React.PropTypes.array.isRequired, onItemPlayClick: React.PropTypes.func.isRequired, + currentItem: React.PropTypes.object.isRequired, } function getDuration(items) { return items.reduce((r, i) => (r + i.duration), 0) } + +const mapStateToProps = (state) => ({ + contexts: selectors.getPlaylistGroupedByContext(state), + currentItem: selectors.currentTrack(state), +}) +const mapDispatchToProps = (dispatch) => ({ + onItemPlayClick: (item) => (dispatch(player.playItem(item))), +}) +export default connect(mapStateToProps, mapDispatchToProps)(PlayQueue) diff --git a/app/l8pr/static/components/PlayQueue/style.scss b/app/l8pr/static/containers/PlayQueue/style.scss similarity index 100% rename from app/l8pr/static/components/PlayQueue/style.scss rename to app/l8pr/static/containers/PlayQueue/style.scss diff --git a/app/l8pr/static/components/PlayQueue/test.js b/app/l8pr/static/containers/PlayQueue/test.js similarity index 100% rename from app/l8pr/static/components/PlayQueue/test.js rename to app/l8pr/static/containers/PlayQueue/test.js diff --git a/app/l8pr/static/containers/Strip/index.js b/app/l8pr/static/containers/Strip/index.js index 5deea2c..e65f69e 100644 --- a/app/l8pr/static/containers/Strip/index.js +++ b/app/l8pr/static/containers/Strip/index.js @@ -1,25 +1,21 @@ import React from 'react' import { connect } from 'react-redux' -import { PlayQueue } from '../../components' +import { PlayQueue } from '../index' import { StripHeader, Controller } from '../index' -import * as selectors from '../../selectors' -import * as player from '../../actions/player' import './style.scss' class Strip extends React.Component { static propTypes = { stripOpened: React.PropTypes.bool, - playlist: React.PropTypes.array, - onItemPlayClick: React.PropTypes.func, } render() { - const { stripOpened, playlist, onItemPlayClick } = this.props + const { stripOpened } = this.props return (
          { stripOpened && - + }
          @@ -29,9 +25,6 @@ class Strip extends React.Component { const mapStateToProps = (state) => ({ stripOpened: state.browser.stripOpened, - playlist: selectors.getPlaylistGroupedByContext(state), }) -const mapDispatchToProps = (dispatch) => ({ - onItemPlayClick: (item) => (dispatch(player.playItem(item))), -}) -export default connect(mapStateToProps, mapDispatchToProps)(Strip) + +export default connect(mapStateToProps)(Strip) diff --git a/app/l8pr/static/containers/index.js b/app/l8pr/static/containers/index.js index 8915933..774e654 100644 --- a/app/l8pr/static/containers/index.js +++ b/app/l8pr/static/containers/index.js @@ -5,3 +5,4 @@ export Strip from './Strip/index' export StripHeader from './StripHeader/index' export Controller from './Controller/index' export { ModalsContainer } from './ModalsContainer' +export PlayQueue from './PlayQueue' From 792f4db64aaa8d1b7da0d7ee5c795363dab6877f Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Thu, 13 Apr 2017 18:48:29 +0200 Subject: [PATCH 022/143] data actions removed --- app/l8pr/static/actions/data.js | 30 ---------- app/l8pr/static/actions/player.js | 53 ++++++++---------- app/l8pr/static/constants/index.js | 1 + app/l8pr/static/index.js | 4 +- app/l8pr/static/reducers/player.js | 5 ++ app/l8pr/static/utils/index.js | 16 +++--- app/l8pr/static/utils/itemsLoaders.js | 79 +++++++++++++++++++++++++++ 7 files changed, 117 insertions(+), 71 deletions(-) delete mode 100644 app/l8pr/static/actions/data.js create mode 100644 app/l8pr/static/utils/itemsLoaders.js diff --git a/app/l8pr/static/actions/data.js b/app/l8pr/static/actions/data.js deleted file mode 100644 index 1a25e9c..0000000 --- a/app/l8pr/static/actions/data.js +++ /dev/null @@ -1,30 +0,0 @@ -import fetch from 'isomorphic-fetch' - -import { SERVER_URL } from '../utils/config' -import { checkHttpStatus, parseJSON } from '../utils' - -export function fetchLastItems({ username }) { - return fetch(`${SERVER_URL}/api/items/?users=${username}&limit=10`, { - // credentials: 'include', - headers: { - Accept: 'application/json', - // Authorization: `Token ${token}` - }, - }) - .then(checkHttpStatus) - .then(parseJSON) - .then((data) => data.results) -} - -export function fetchUserShows({ username }) { - return fetch(`${SERVER_URL}/api/loops/?username=${username}`, { - // credentials: 'include', - headers: { - Accept: 'application/json', - // Authorization: `Token ${token}` - }, - }) - .then(checkHttpStatus) - .then(parseJSON) - .then((items) => items[0].shows_list) -} diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index 591d909..5cedcc5 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -1,7 +1,7 @@ -import { fetchLastItems, fetchUserShows } from './data' import * as c from '../constants' import * as selectors from '../selectors' import { push } from 'react-router-redux' +import * as contextLoaders from '../utils/itemsLoaders' export function setPlaylist(playlist) { return { @@ -10,38 +10,31 @@ export function setPlaylist(playlist) { } } -export function fillQueueList() { +export function appendToPlaylist(items) { + return { + type: c.APPEND_TO_PLAYLIST, + payload: items, + } +} +export function fillQueueList() { + return (dispatch, getState) => ( + contextLoaders.lastItemsInLoopr({ excludeIds: selectors.history(getState()).map((i) => i.id) }) + .then((items) => ( + dispatch(appendToPlaylist(items)) + )) + ) } -export function initQueueList({ user, queue, item }) { +export function initQueueList() { return (dispatch, getState) => { - const username = user && user || selectors.currentUser(getState()).username - if (!initQueueList) {return} + const inUrl = selectors.getLocation(getState()) + const username = inUrl.user || selectors.currentUser(getState()).username return Promise.all([ - // LAST ITEMS - fetchLastItems({ username: username }) - // set context - .then((items) => (items.map((i) => ({ - ...i, - context: { title: 'Last Items' }, - })))), + // LAST USER ITEMS + contextLoaders.lastUserItems({ username }), // SHOWS - fetchUserShows({ username: username }) - .then((shows) => ( - shows.map((show) => ( - // set context - show.items.map((i) => ({ - ...i, - context: { - ...show, - items: null, - }, - })) - )) - )) - // flatten items - .then((showsItems) => ([].concat.apply([], showsItems))), + contextLoaders.shows({ username }), ]) // flatten .then((results) => ([].concat.apply([], results))) @@ -73,9 +66,9 @@ export function play() { url += `/item/${item.id}` } dispatch(push(url)) - // if (selectors.getPlaylistGroupedByContext(getState()).length < 3) { - // dispatch(fillQueueList()) - // } + if (selectors.getPlaylistGroupedByContext(getState()).length < 2) { + dispatch(fillQueueList()) + } } } diff --git a/app/l8pr/static/constants/index.js b/app/l8pr/static/constants/index.js index 5dae38c..42c415f 100644 --- a/app/l8pr/static/constants/index.js +++ b/app/l8pr/static/constants/index.js @@ -23,3 +23,4 @@ export const RECEIVE_LOOP = 'RECEIVE_LOOP' export const SET_PLAYLIST = 'SET_PLAYLIST' export const SET_STRIP_STATE = 'SET_STRIP_STATE' +export const APPEND_TO_PLAYLIST = 'APPEND_TO_PLAYLIST' diff --git a/app/l8pr/static/index.js b/app/l8pr/static/index.js index 4906c90..f41441d 100644 --- a/app/l8pr/static/index.js +++ b/app/l8pr/static/index.js @@ -44,9 +44,7 @@ if (token !== null) { } // init playqueue from url -store.dispatch(player.initQueueList( - selectors.getLocation(store.getState()) -)) +store.dispatch(player.initQueueList()) ReactDOM.render(node, target) diff --git a/app/l8pr/static/reducers/player.js b/app/l8pr/static/reducers/player.js index 12d761f..9f88c51 100644 --- a/app/l8pr/static/reducers/player.js +++ b/app/l8pr/static/reducers/player.js @@ -67,6 +67,11 @@ export default function (state = { ...state, playQueue: action.payload, } + case c.APPEND_TO_PLAYLIST: + return { + ...state, + playQueue: [...state.playQueue, ...action.payload], + } case c.PLAY: return { ...state, diff --git a/app/l8pr/static/utils/index.js b/app/l8pr/static/utils/index.js index 2a763d7..5531dab 100644 --- a/app/l8pr/static/utils/index.js +++ b/app/l8pr/static/utils/index.js @@ -1,20 +1,20 @@ export function createReducer(initialState, reducerMap) { return (state = initialState, action) => { - const reducer = reducerMap[action.type]; - return reducer ? reducer(state, action.payload) : state; - }; + const reducer = reducerMap[action.type] + return reducer ? reducer(state, action.payload) : state + } } export function checkHttpStatus(response) { if (response.status >= 200 && response.status < 300) { - return response; + return response } - const error = new Error(response.statusText); - error.response = response; - throw error; + const error = new Error(response.statusText) + error.response = response + throw error } export function parseJSON(response) { - return response.json(); + return response.json() } diff --git a/app/l8pr/static/utils/itemsLoaders.js b/app/l8pr/static/utils/itemsLoaders.js new file mode 100644 index 0000000..e18892e --- /dev/null +++ b/app/l8pr/static/utils/itemsLoaders.js @@ -0,0 +1,79 @@ +import fetch from 'isomorphic-fetch' +import { SERVER_URL } from '../utils/config' +import { checkHttpStatus, parseJSON } from './index' + +function fetchLastItems({ username, count=10 }) { + let url = `${SERVER_URL}/api/items/?limit=${count}&ordering=-added` + if (username) url += `&users=${username}` + return fetch(url, { + // credentials: 'include', + headers: { + Accept: 'application/json', + // Authorization: `Token ${token}` + }, + }) + .then(checkHttpStatus) + .then(parseJSON) + .then((data) => data.results) +} + +function fetchUserShows({ username }) { + return fetch(`${SERVER_URL}/api/loops/?username=${username}`, { + // credentials: 'include', + headers: { + Accept: 'application/json', + // Authorization: `Token ${token}` + }, + }) + .then(checkHttpStatus) + .then(parseJSON) + .then((items) => items[0].shows_list) +} + +export const lastItemsInLoopr = () => ( + fetchLastItems({ count: 10 }) + // set a context + .then((items) => ( + items.map((i) => ( + { + ...i, + context: { + title: 'Last items in loopr', + id: 'last_items_in_loopr', + }, + } + )) + )) +) + +export const shows = ({ username }) => ( + fetchUserShows({ username }) + .then((shows) => ( + shows.map((show) => ( + // set context + show.items.map((i) => ({ + ...i, + context: { + ...show, + items: null, + }, + })) + )) + )) + // flatten items + .then((showsItems) => ([].concat.apply([], showsItems))) +) + +export const lastUserItems = ({ username }) => { + return fetchLastItems({ username }) + // set context + .then((items) => ( + items.map((i) => ({ + ...i, + context: { + title: 'Last Items', + id: 'user_last_item', + }, + })) + )) +} From 0e66d2b04ba9cbd9950ad3bfb2d356fa4d910eac Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Fri, 14 Apr 2017 23:06:49 +0200 Subject: [PATCH 023/143] last 10 --- app/l8pr/api.py | 2 +- .../migrations/0024_auto_20170413_1653.py | 27 +++++++++++++++++++ app/l8pr/static/actions/player.js | 4 +-- app/l8pr/static/utils/itemsLoaders.js | 2 +- 4 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 app/l8pr/migrations/0024_auto_20170413_1653.py diff --git a/app/l8pr/api.py b/app/l8pr/api.py index d1d7e92..ea66957 100644 --- a/app/l8pr/api.py +++ b/app/l8pr/api.py @@ -180,7 +180,7 @@ class ShowViewSet(viewsets.ModelViewSet): class ItemViewSet(viewsets.ModelViewSet): queryset = Item.objects.all() serializer_class = ItemSerializer - filter_fields = ('url', 'users') + filter_fields = ('url', 'users', 'users__username') class ItemSearchSerializer(HaystackSerializer): diff --git a/app/l8pr/migrations/0024_auto_20170413_1653.py b/app/l8pr/migrations/0024_auto_20170413_1653.py new file mode 100644 index 0000000..5d69059 --- /dev/null +++ b/app/l8pr/migrations/0024_auto_20170413_1653.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-04-13 16:53 +from __future__ import unicode_literals + +from django.db import migrations + + +def link_items_to_users(apps, schema_editor): + Show = apps.get_model('l8pr', 'Show') + ItemsUsersRelationship = apps.get_model('l8pr', 'ItemsUsersRelationship') + from app.l8pr.models import Show, ItemsUsersRelationship + for s in Show.objects.all(): + for i in s.items.all(): + if s.user not in i.users.all(): + rel = ItemsUsersRelationship(item=i, user=s.user) + rel.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('l8pr', '0023_auto_20170405_1342'), + ] + + operations = [ + migrations.RunPython(link_items_to_users), + ] diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index 5cedcc5..359b9e0 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -28,8 +28,8 @@ export function fillQueueList() { export function initQueueList() { return (dispatch, getState) => { - const inUrl = selectors.getLocation(getState()) - const username = inUrl.user || selectors.currentUser(getState()).username + const location = selectors.getLocation(getState()) + const username = location.user || selectors.currentUser(getState()).username return Promise.all([ // LAST USER ITEMS contextLoaders.lastUserItems({ username }), diff --git a/app/l8pr/static/utils/itemsLoaders.js b/app/l8pr/static/utils/itemsLoaders.js index e18892e..bfaf6ac 100644 --- a/app/l8pr/static/utils/itemsLoaders.js +++ b/app/l8pr/static/utils/itemsLoaders.js @@ -4,7 +4,7 @@ import { checkHttpStatus, parseJSON } from './index' function fetchLastItems({ username, count=10 }) { let url = `${SERVER_URL}/api/items/?limit=${count}&ordering=-added` - if (username) url += `&users=${username}` + if (username) url += `&users__username=${username}` return fetch(url, { // credentials: 'include', headers: { From 915436a552e8cf1ac76e7aa57f722447e270d34d Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Fri, 14 Apr 2017 23:13:39 +0200 Subject: [PATCH 024/143] cover clickable --- app/l8pr/static/containers/PlayQueue/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/l8pr/static/containers/PlayQueue/index.js b/app/l8pr/static/containers/PlayQueue/index.js index a5ce530..538c810 100644 --- a/app/l8pr/static/containers/PlayQueue/index.js +++ b/app/l8pr/static/containers/PlayQueue/index.js @@ -25,7 +25,7 @@ class PlayQueue extends React.Component {
        {c.items.slice(0, 15).map((i, idx)=> ( - + ))}
        From 0b2c2f485778c8670a6539eb7ce84767b648cd82 Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Sat, 15 Apr 2017 20:21:09 +0200 Subject: [PATCH 025/143] progress bar --- app/l8pr/static/actions/player.js | 2 +- .../static/components/Progressbar/index.js | 26 ++++++++++++++ .../static/components/Progressbar/style.scss | 4 +++ app/l8pr/static/components/Screen/index.js | 5 +++ app/l8pr/static/components/index.js | 1 + app/l8pr/static/containers/Home/index.js | 34 +++++++++++++------ app/l8pr/static/containers/Strip/index.js | 9 +++-- app/l8pr/static/utils/itemsLoaders.js | 2 +- 8 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 app/l8pr/static/components/Progressbar/index.js create mode 100644 app/l8pr/static/components/Progressbar/style.scss diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index 359b9e0..0f32319 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -19,7 +19,7 @@ export function appendToPlaylist(items) { export function fillQueueList() { return (dispatch, getState) => ( - contextLoaders.lastItemsInLoopr({ excludeIds: selectors.history(getState()).map((i) => i.id) }) + contextLoaders.lastItemsInLoopr() .then((items) => ( dispatch(appendToPlaylist(items)) )) diff --git a/app/l8pr/static/components/Progressbar/index.js b/app/l8pr/static/components/Progressbar/index.js new file mode 100644 index 0000000..9f0a44f --- /dev/null +++ b/app/l8pr/static/components/Progressbar/index.js @@ -0,0 +1,26 @@ +import React from 'react' +import './style.scss' + +export default class Progressbar extends React.Component { + + + onClick = (e) => { + this.props.onClick(e.nativeEvent.offsetX / this.refs.progressbar.clientWidth) + } + + render() { + const { value, loaded } = this.props + return ( +
        +
        +
        +
        + ) + } +} + +Progressbar.propTypes = { + value: React.PropTypes.number, + loaded: React.PropTypes.number, + onClick: React.PropTypes.func, +} diff --git a/app/l8pr/static/components/Progressbar/style.scss b/app/l8pr/static/components/Progressbar/style.scss new file mode 100644 index 0000000..bca2d87 --- /dev/null +++ b/app/l8pr/static/components/Progressbar/style.scss @@ -0,0 +1,4 @@ +.Progressbar__progress { + height: 10px; + background-color: blue; +} diff --git a/app/l8pr/static/components/Screen/index.js b/app/l8pr/static/components/Screen/index.js index 720a253..afe1a17 100644 --- a/app/l8pr/static/components/Screen/index.js +++ b/app/l8pr/static/components/Screen/index.js @@ -4,10 +4,15 @@ import './style.scss' export default class Screen extends React.Component { + seekTo(value) { + this.refs.reactPlayer.seekTo(value) + } + render() { return ( diff --git a/app/l8pr/static/components/index.js b/app/l8pr/static/components/index.js index 11cd4d0..3bead3c 100644 --- a/app/l8pr/static/components/index.js +++ b/app/l8pr/static/components/index.js @@ -1,3 +1,4 @@ export Screen from './Screen' export ListItem from './ListItem' export LoginModal from './LoginModal' +export Progressbar from './Progressbar' diff --git a/app/l8pr/static/containers/Home/index.js b/app/l8pr/static/containers/Home/index.js index a734e15..afce274 100644 --- a/app/l8pr/static/containers/Home/index.js +++ b/app/l8pr/static/containers/Home/index.js @@ -12,12 +12,21 @@ class HomeView extends React.Component { static propTypes = { media: React.PropTypes.object, playing: React.PropTypes.bool, - nextItem: React.PropTypes.func, - play: React.PropTypes.func, - pause: React.PropTypes.func, + onEnd: React.PropTypes.func, + onPlay: React.PropTypes.func, + onPause: React.PropTypes.func, volume: React.PropTypes.number, } + constructor(props) { + super(props) + this.state = { progress: 0 } + } + + onSeekTo = (value) => { + this.refs.player.seekTo(value) + } + render() { return (
        @@ -26,14 +35,17 @@ class HomeView extends React.Component { + onEnded={this.props.onEnd} + onPlay={this.props.onPlay} + onPause={this.props.onPause} + onProgress={(p)=>(this.setState({ progress: p.played }))} + onReady={()=>(this.setState({ progress: 0 }))} + onError={this.props.onEnd}/> } - +
        ) } @@ -46,9 +58,9 @@ const mapStateToProps = (state) => ({ }) const mapDispatchToProps = (dispatch) => ({ - nextItem: () => (dispatch(next())), - play: (args) => (dispatch(play(args))), - pause: (args) => (dispatch(pause(args))), + onEnd: () => (dispatch(next())), + onPlay: (args) => (dispatch(play(args))), + onPause: (args) => (dispatch(pause(args))), }) export default connect(mapStateToProps, mapDispatchToProps)(HomeView) diff --git a/app/l8pr/static/containers/Strip/index.js b/app/l8pr/static/containers/Strip/index.js index e65f69e..b5dceb8 100644 --- a/app/l8pr/static/containers/Strip/index.js +++ b/app/l8pr/static/containers/Strip/index.js @@ -1,22 +1,25 @@ import React from 'react' import { connect } from 'react-redux' -import { PlayQueue } from '../index' -import { StripHeader, Controller } from '../index' +import { Progressbar } from '../../components' +import { StripHeader, Controller, PlayQueue } from '../index' import './style.scss' class Strip extends React.Component { static propTypes = { stripOpened: React.PropTypes.bool, + progress: React.PropTypes.number, + onSeekTo: React.PropTypes.func, } render() { - const { stripOpened } = this.props + const { stripOpened, progress, onSeekTo } = this.props return (
        { stripOpened && } +
        ) diff --git a/app/l8pr/static/utils/itemsLoaders.js b/app/l8pr/static/utils/itemsLoaders.js index bfaf6ac..c0a7b8d 100644 --- a/app/l8pr/static/utils/itemsLoaders.js +++ b/app/l8pr/static/utils/itemsLoaders.js @@ -72,7 +72,7 @@ export const lastUserItems = ({ username }) => { ...i, context: { title: 'Last Items', - id: 'user_last_item', + id: `${username}_last_item`, }, })) )) From a54b8eabd96ae67dcda8795f80c08c83c91958d8 Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Sat, 15 Apr 2017 21:01:33 +0200 Subject: [PATCH 026/143] loaded in progress bar --- .../static/components/Progressbar/index.js | 6 ++--- .../static/components/Progressbar/style.scss | 12 +++++++++- app/l8pr/static/containers/Home/index.js | 22 ++++++++++++++----- app/l8pr/static/containers/Strip/index.js | 5 +++-- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/app/l8pr/static/components/Progressbar/index.js b/app/l8pr/static/components/Progressbar/index.js index 9f0a44f..bb48a44 100644 --- a/app/l8pr/static/components/Progressbar/index.js +++ b/app/l8pr/static/components/Progressbar/index.js @@ -9,18 +9,18 @@ export default class Progressbar extends React.Component { } render() { - const { value, loaded } = this.props + const { progress, loaded } = this.props return (
        -
        +
        ) } } Progressbar.propTypes = { - value: React.PropTypes.number, + progress: React.PropTypes.number, loaded: React.PropTypes.number, onClick: React.PropTypes.func, } diff --git a/app/l8pr/static/components/Progressbar/style.scss b/app/l8pr/static/components/Progressbar/style.scss index bca2d87..96e8493 100644 --- a/app/l8pr/static/components/Progressbar/style.scss +++ b/app/l8pr/static/components/Progressbar/style.scss @@ -1,4 +1,14 @@ -.Progressbar__progress { +.Progressbar, .Progressbar__progress, .Progressbar__loaded { height: 10px; +} +.Progressbar { + position: relative; +} +.Progressbar__progress { background-color: blue; + position: absolute; + top: 0; +} +.Progressbar__loaded { + background-color: green; } diff --git a/app/l8pr/static/containers/Home/index.js b/app/l8pr/static/containers/Home/index.js index afce274..83488c7 100644 --- a/app/l8pr/static/containers/Home/index.js +++ b/app/l8pr/static/containers/Home/index.js @@ -20,7 +20,10 @@ class HomeView extends React.Component { constructor(props) { super(props) - this.state = { progress: 0 } + this.state = { + progress: 0, + loaded: 0, + } } onSeekTo = (value) => { @@ -39,13 +42,22 @@ class HomeView extends React.Component { playing={this.props.playing} volume={this.props.volume} onEnded={this.props.onEnd} + onError={this.props.onEnd} onPlay={this.props.onPlay} onPause={this.props.onPause} - onProgress={(p)=>(this.setState({ progress: p.played }))} - onReady={()=>(this.setState({ progress: 0 }))} - onError={this.props.onEnd}/> + onProgress={(p)=>(this.setState({ + progress: p.played, + loaded: p.loaded, + }))} + onReady={()=>(this.setState({ + progress: 0, + loaded: 0, + }))}/> } - +
        ) } diff --git a/app/l8pr/static/containers/Strip/index.js b/app/l8pr/static/containers/Strip/index.js index b5dceb8..9915113 100644 --- a/app/l8pr/static/containers/Strip/index.js +++ b/app/l8pr/static/containers/Strip/index.js @@ -8,18 +8,19 @@ class Strip extends React.Component { static propTypes = { stripOpened: React.PropTypes.bool, progress: React.PropTypes.number, + loaded: React.PropTypes.number, onSeekTo: React.PropTypes.func, } render() { - const { stripOpened, progress, onSeekTo } = this.props + const { stripOpened, progress, loaded, onSeekTo } = this.props return (
        { stripOpened && } - +
        ) From 5e605c4d741d8e55a4cea81c6ea63e9167e8dd04 Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Sat, 15 Apr 2017 21:39:40 +0200 Subject: [PATCH 027/143] m to mute --- app/l8pr/static/actions/player.js | 10 ++++++++++ app/l8pr/static/index.js | 2 ++ 2 files changed, 12 insertions(+) diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index 0f32319..2b9e9fa 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -82,6 +82,16 @@ export function togglePlay() { } } +export function toggleMute() { + return (dispatch, getState) => { + if (getState().player.muted) { + dispatch(unmute()) + } else { + dispatch(mute()) + } + } +} + export function previousContext() { return { type: c.PREVIOUS_CONTEXT } } diff --git a/app/l8pr/static/index.js b/app/l8pr/static/index.js index f41441d..7937706 100644 --- a/app/l8pr/static/index.js +++ b/app/l8pr/static/index.js @@ -19,9 +19,11 @@ const history = syncHistoryWithStore(browserHistory, store) const map = { toggleStrip: 'c', playPause: 'space', + toggleMute: 'm', } const handlers = { toggleStrip: () => store.dispatch(browser.toggleStrip()), + toggleMute: () => store.dispatch(player.toggleMute()), playPause: () => store.dispatch(player.togglePlay()), } const node = ( From 0263972ad073ff808fd89fe294dd12039fbdc037 Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Sat, 15 Apr 2017 23:13:24 +0200 Subject: [PATCH 028/143] current title in navplayer --- .../static/components/NavPlayer/style.scss | 9 ----- .../static/components/Progressbar/style.scss | 3 +- .../static/containers/Controller/index.js | 27 -------------- .../NavPlayer/index.js | 35 +++++++++++++++++-- .../static/containers/NavPlayer/style.scss | 33 +++++++++++++++++ app/l8pr/static/containers/Strip/index.js | 4 +-- .../static/containers/StripHeader/index.js | 8 ++--- .../static/containers/StripHeader/style.scss | 22 ------------ app/l8pr/static/containers/index.js | 2 +- 9 files changed, 73 insertions(+), 70 deletions(-) delete mode 100644 app/l8pr/static/components/NavPlayer/style.scss delete mode 100644 app/l8pr/static/containers/Controller/index.js rename app/l8pr/static/{components => containers}/NavPlayer/index.js (60%) create mode 100644 app/l8pr/static/containers/NavPlayer/style.scss diff --git a/app/l8pr/static/components/NavPlayer/style.scss b/app/l8pr/static/components/NavPlayer/style.scss deleted file mode 100644 index 4b032be..0000000 --- a/app/l8pr/static/components/NavPlayer/style.scss +++ /dev/null @@ -1,9 +0,0 @@ -.NavPlayer { - background-color: $colorPlaying; - padding: 10px; - color: white; -} - -.NavPlayer a { - color: white; -} diff --git a/app/l8pr/static/components/Progressbar/style.scss b/app/l8pr/static/components/Progressbar/style.scss index 96e8493..de033e7 100644 --- a/app/l8pr/static/components/Progressbar/style.scss +++ b/app/l8pr/static/components/Progressbar/style.scss @@ -3,6 +3,7 @@ } .Progressbar { position: relative; + background-color: rgba(255, 255, 255, .6); } .Progressbar__progress { background-color: blue; @@ -10,5 +11,5 @@ top: 0; } .Progressbar__loaded { - background-color: green; + background-color: lighten(blue, 20%); } diff --git a/app/l8pr/static/containers/Controller/index.js b/app/l8pr/static/containers/Controller/index.js deleted file mode 100644 index 9549903..0000000 --- a/app/l8pr/static/containers/Controller/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react' -import { connect } from 'react-redux' -import NavPlayer from '../../components/NavPlayer' -import * as player from '../../actions/player' - -function ControllerComponent(props) { - return ( - - ) -} - -const mapStateToProps = (state) => ({ - playing: state.player.playing, - muted: state.player.muted, -}) - -const mapDispatchToProps = (dispatch) => ({ - onPlay: () => (dispatch(player.play())), - onPause: () => (dispatch(player.pause())), - onNextItem: () => (dispatch(player.next())), - onNextContext: () => (dispatch(player.nextContext())), - onPreviousItem: () => (dispatch(player.previous())), - onPreviousContext: () => (dispatch(player.previousContext())), - onMute: () => (dispatch(player.mute())), - onUnmute: () => (dispatch(player.unmute())), -}) -export default connect(mapStateToProps, mapDispatchToProps)(ControllerComponent) diff --git a/app/l8pr/static/components/NavPlayer/index.js b/app/l8pr/static/containers/NavPlayer/index.js similarity index 60% rename from app/l8pr/static/components/NavPlayer/index.js rename to app/l8pr/static/containers/NavPlayer/index.js index cb61a2c..b161331 100644 --- a/app/l8pr/static/components/NavPlayer/index.js +++ b/app/l8pr/static/containers/NavPlayer/index.js @@ -1,7 +1,11 @@ import React from 'react' +import { connect } from 'react-redux' +import * as player from '../../actions/player' +import * as selectors from '../../selectors' +import { get } from 'lodash' import './style.scss' -export default function NavPlayer({ +function NavPlayer({ onPreviousContext, onPreviousItem, onPlay, @@ -12,10 +16,16 @@ export default function NavPlayer({ onUnmute, muted, playing, + currentItem, + currentShow, }) { return (
        -
        +
        +
        {get(currentItem, 'title')}
        +
        {get(currentShow, 'title')}
        +
        +
        first_page @@ -62,4 +72,25 @@ NavPlayer.propTypes = { onUnmute: React.PropTypes.func, playing: React.PropTypes.bool, muted: React.PropTypes.bool, + currentItem: React.PropTypes.string, + currentShow: React.PropTypes.string, } + +const mapStateToProps = (state) => ({ + playing: state.player.playing, + muted: state.player.muted, + currentItem: selectors.currentTrack(state), + currentShow: selectors.currentShow(state), +}) + +const mapDispatchToProps = (dispatch) => ({ + onPlay: () => (dispatch(player.play())), + onPause: () => (dispatch(player.pause())), + onNextItem: () => (dispatch(player.next())), + onNextContext: () => (dispatch(player.nextContext())), + onPreviousItem: () => (dispatch(player.previous())), + onPreviousContext: () => (dispatch(player.previousContext())), + onMute: () => (dispatch(player.mute())), + onUnmute: () => (dispatch(player.unmute())), +}) +export default connect(mapStateToProps, mapDispatchToProps)(NavPlayer) diff --git a/app/l8pr/static/containers/NavPlayer/style.scss b/app/l8pr/static/containers/NavPlayer/style.scss new file mode 100644 index 0000000..e32683d --- /dev/null +++ b/app/l8pr/static/containers/NavPlayer/style.scss @@ -0,0 +1,33 @@ +.NavPlayer { + background-color: $colorPlaying; + padding: 10px; + color: white; +} + +.NavPlayer a { + color: white; +} + +.NavPlayer__controllers { + min-width: 180px; + white-space: nowrap; +} + +.NavPlayer__item, .NavPlayer__show { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: normal; + font-weight: 500; + width: 100%; +} + +.NavPlayer__item { + font-family: $primaryFont; + font-size: 2em; +} + +.NavPlayer__show { + font-family: $secondaryFont; + font-size: 1em; +} diff --git a/app/l8pr/static/containers/Strip/index.js b/app/l8pr/static/containers/Strip/index.js index 9915113..a95c9f8 100644 --- a/app/l8pr/static/containers/Strip/index.js +++ b/app/l8pr/static/containers/Strip/index.js @@ -1,7 +1,7 @@ import React from 'react' import { connect } from 'react-redux' import { Progressbar } from '../../components' -import { StripHeader, Controller, PlayQueue } from '../index' +import { StripHeader, NavPlayer, PlayQueue } from '../index' import './style.scss' class Strip extends React.Component { @@ -21,7 +21,7 @@ class Strip extends React.Component { } - +
        ) } diff --git a/app/l8pr/static/containers/StripHeader/index.js b/app/l8pr/static/containers/StripHeader/index.js index 1f06f0b..555d949 100644 --- a/app/l8pr/static/containers/StripHeader/index.js +++ b/app/l8pr/static/containers/StripHeader/index.js @@ -6,12 +6,10 @@ import { showModal } from '../../actions/modal' import { get } from 'lodash' import './style.scss' -function StripHeaderComponent({ trackTitle, stripOpened, toggleStrip, showTitle, onLogin }) { +function StripHeaderComponent({ stripOpened, toggleStrip, onLogin }) { return (
        -
        -
        {trackTitle}

        -
        {showTitle}
        +
        @@ -25,8 +23,6 @@ function StripHeaderComponent({ trackTitle, stripOpened, toggleStrip, showTitle, } const mapStateToProps = (state) => ({ - trackTitle: get(selectors.currentTrack(state), 'title'), - showTitle: get(selectors.currentShow(state), 'title'), stripOpened: state.browser.stripOpened, }) diff --git a/app/l8pr/static/containers/StripHeader/style.scss b/app/l8pr/static/containers/StripHeader/style.scss index 296f786..e2ca27b 100644 --- a/app/l8pr/static/containers/StripHeader/style.scss +++ b/app/l8pr/static/containers/StripHeader/style.scss @@ -2,25 +2,3 @@ background-color: white; padding: 10px; } -.StripHeader__playingNow { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: normal; - padding-top: 18px; - font-weight: 500; - text-align: left; -} - -.StripHeader__playingNow__item { - color: $colorPlaying; - font-family: $primaryFont; - font-size: 2em; - display: inline-block; -} - -.StripHeader__playingNow__show { - font-family: $secondaryFont; - font-size: 1em; - display: inline-block; -} diff --git a/app/l8pr/static/containers/index.js b/app/l8pr/static/containers/index.js index 774e654..a018d87 100644 --- a/app/l8pr/static/containers/index.js +++ b/app/l8pr/static/containers/index.js @@ -2,7 +2,7 @@ export HomeView from './Home/index' export LoginView from './Login/index' export NotFoundView from './NotFound/index' export Strip from './Strip/index' +export NavPlayer from './NavPlayer/index' export StripHeader from './StripHeader/index' -export Controller from './Controller/index' export { ModalsContainer } from './ModalsContainer' export PlayQueue from './PlayQueue' From 0703602f4a5ab84ed1522457e06083976eb3a679 Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Wed, 19 Apr 2017 09:16:20 +0200 Subject: [PATCH 029/143] search --- app/l8pr/static/actions/browser.js | 38 +++----------- .../static/components/ContextsList/index.js | 49 +++++++++++++++++++ .../static/components/ContextsList/style.scss | 43 ++++++++++++++++ app/l8pr/static/components/index.js | 1 + app/l8pr/static/constants/index.js | 1 + app/l8pr/static/containers/Browser/index.js | 18 +++++++ app/l8pr/static/containers/NavPlayer/index.js | 16 +++++- app/l8pr/static/containers/PlayQueue/index.js | 41 +++------------- app/l8pr/static/containers/Search/index.js | 33 +++++++++++++ app/l8pr/static/containers/Search/style.scss | 0 app/l8pr/static/containers/Strip/index.js | 7 +-- .../static/containers/StripHeader/index.js | 23 +++++---- app/l8pr/static/containers/index.js | 4 +- app/l8pr/static/reducers/browser.js | 10 ++-- 14 files changed, 197 insertions(+), 87 deletions(-) create mode 100644 app/l8pr/static/components/ContextsList/index.js create mode 100644 app/l8pr/static/components/ContextsList/style.scss create mode 100644 app/l8pr/static/containers/Browser/index.js create mode 100644 app/l8pr/static/containers/Search/index.js create mode 100644 app/l8pr/static/containers/Search/style.scss diff --git a/app/l8pr/static/actions/browser.js b/app/l8pr/static/actions/browser.js index 1e7d2b2..ae76d75 100644 --- a/app/l8pr/static/actions/browser.js +++ b/app/l8pr/static/actions/browser.js @@ -1,18 +1,12 @@ -import fetch from 'isomorphic-fetch'; -import { push } from 'react-router-redux'; +import * as c from '../constants' -import { SERVER_URL } from '../utils/config'; -import { checkHttpStatus, parseJSON } from '../utils'; -import * as c from '../constants'; -import { authLoginUserFailure } from './auth'; - -export function openStrip(){ +export function openStrip() { return { type: c.SET_STRIP_STATE, payload: true, } } -export function closeStrip(){ +export function closeStrip() { return { type: c.SET_STRIP_STATE, payload: false, @@ -27,28 +21,10 @@ export function toggleStrip() { } } } -function receiveLoop(loop) { +export function browse(browserType, browserProps) { return { - type: c.RECEIVE_LOOP, - payload: loop, + type: c.BROWSE, + browserType, + browserProps, } } - -export function fetchShows(userId=2) { - return (dispatch) => ( - fetch(`${SERVER_URL}/api/loops/${userId}/`, { - // credentials: 'include', - headers: { - Accept: 'application/json', - // Authorization: `Token ${token}` - } - }) - .then(checkHttpStatus) - .then(parseJSON) - .then((response) => { - const loop = response.shows_list - dispatch(receiveLoop(loop)) - return loop - }) - ) -} diff --git a/app/l8pr/static/components/ContextsList/index.js b/app/l8pr/static/components/ContextsList/index.js new file mode 100644 index 0000000..5a5836b --- /dev/null +++ b/app/l8pr/static/components/ContextsList/index.js @@ -0,0 +1,49 @@ +import React from 'react' +import { ListItem } from '../../components' +import moment from 'moment' +import './style.scss' + +export default function ContextsList({ contexts, onItemPlayClick, currentItem }) { + return ( +
        +
          + {contexts.map((c) => ( +
        • +
          +
          + {c.context.title}
          + {c.items.length} items / + {moment.duration(getDuration(c.items), 's').humanize()} +
          +
          + {c.items.slice(0, 15).map((i, idx)=> ( + + ))} +
          +
          +
            + {c.items.map(i => ( +
          1. + +
          2. + ))} +
          +
        • + ))} +
        +
        + ) +} + +ContextsList.propTypes = { + contexts: React.PropTypes.array.isRequired, + onItemPlayClick: React.PropTypes.func.isRequired, + currentItem: React.PropTypes.object.isRequired, +} + +function getDuration(items) { + return items.reduce((r, i) => (r + i.duration), 0) +} diff --git a/app/l8pr/static/components/ContextsList/style.scss b/app/l8pr/static/components/ContextsList/style.scss new file mode 100644 index 0000000..2cdec72 --- /dev/null +++ b/app/l8pr/static/components/ContextsList/style.scss @@ -0,0 +1,43 @@ +.ContextsList { + max-height: 55vh; + overflow: auto; +} + +.context { + border-bottom: 10px solid $colorShow; +} +.context__cover { + vertical-align: top; + display: inline-block; + padding: 15px 25px; + background-color: white; + color: $colorShow; + display: flex; +} + +.context__illustrations { + padding-left: 20px; + white-space: nowrap; + overflow: hidden; + max-height: 100px; + flex-shrink: 3; + text-align: right; + img { + background-size: cover; + height: 100%; + } +} + +.context__title { + font-family: $secondaryFont; + font-size: 2em; + flex-grow: 1; + width: auto; +} + +.context__details { + font-family: $primaryFont; + font-size: 0.5em; + white-space: nowrap; + +} diff --git a/app/l8pr/static/components/index.js b/app/l8pr/static/components/index.js index 3bead3c..9043b7c 100644 --- a/app/l8pr/static/components/index.js +++ b/app/l8pr/static/components/index.js @@ -2,3 +2,4 @@ export Screen from './Screen' export ListItem from './ListItem' export LoginModal from './LoginModal' export Progressbar from './Progressbar' +export ContextsList from './ContextsList' diff --git a/app/l8pr/static/constants/index.js b/app/l8pr/static/constants/index.js index 42c415f..241966c 100644 --- a/app/l8pr/static/constants/index.js +++ b/app/l8pr/static/constants/index.js @@ -24,3 +24,4 @@ export const RECEIVE_LOOP = 'RECEIVE_LOOP' export const SET_PLAYLIST = 'SET_PLAYLIST' export const SET_STRIP_STATE = 'SET_STRIP_STATE' export const APPEND_TO_PLAYLIST = 'APPEND_TO_PLAYLIST' +export const BROWSE = 'BROWSE' diff --git a/app/l8pr/static/containers/Browser/index.js b/app/l8pr/static/containers/Browser/index.js new file mode 100644 index 0000000..13314b3 --- /dev/null +++ b/app/l8pr/static/containers/Browser/index.js @@ -0,0 +1,18 @@ +import React from 'react' +import { connect } from 'react-redux' +import { PlayQueue, Search } from '../index' + + +function Browser({ browserType }) { + const types = { + 'SEARCH': , + 'PLAYQUEUE': , + } + return types[browserType] +} + +const mapStateToProps = (state) => ({ + browserType: state.browser.browserType, +}) + +export default connect(mapStateToProps)(Browser) diff --git a/app/l8pr/static/containers/NavPlayer/index.js b/app/l8pr/static/containers/NavPlayer/index.js index b161331..9ae1ab9 100644 --- a/app/l8pr/static/containers/NavPlayer/index.js +++ b/app/l8pr/static/containers/NavPlayer/index.js @@ -1,5 +1,6 @@ import React from 'react' import { connect } from 'react-redux' +import * as browser from '../../actions/browser' import * as player from '../../actions/player' import * as selectors from '../../selectors' import { get } from 'lodash' @@ -18,6 +19,8 @@ function NavPlayer({ playing, currentItem, currentShow, + stripOpened, + toggleStrip, }) { return (
        @@ -56,6 +59,10 @@ function NavPlayer({ volume_up } + + {!stripOpened && 'keyboard_arrow_up'} + {stripOpened && 'keyboard_arrow_down'} +
        ) @@ -72,8 +79,10 @@ NavPlayer.propTypes = { onUnmute: React.PropTypes.func, playing: React.PropTypes.bool, muted: React.PropTypes.bool, - currentItem: React.PropTypes.string, - currentShow: React.PropTypes.string, + currentItem: React.PropTypes.object, + currentShow: React.PropTypes.object, + stripOpened: React.PropTypes.bool, + toggleStrip: React.PropTypes.func, } const mapStateToProps = (state) => ({ @@ -81,6 +90,7 @@ const mapStateToProps = (state) => ({ muted: state.player.muted, currentItem: selectors.currentTrack(state), currentShow: selectors.currentShow(state), + stripOpened: state.browser.stripOpened, }) const mapDispatchToProps = (dispatch) => ({ @@ -92,5 +102,7 @@ const mapDispatchToProps = (dispatch) => ({ onPreviousContext: () => (dispatch(player.previousContext())), onMute: () => (dispatch(player.mute())), onUnmute: () => (dispatch(player.unmute())), + toggleStrip: () => (dispatch(browser.toggleStrip())), }) + export default connect(mapStateToProps, mapDispatchToProps)(NavPlayer) diff --git a/app/l8pr/static/containers/PlayQueue/index.js b/app/l8pr/static/containers/PlayQueue/index.js index 538c810..1e57c1f 100644 --- a/app/l8pr/static/containers/PlayQueue/index.js +++ b/app/l8pr/static/containers/PlayQueue/index.js @@ -1,6 +1,6 @@ import React from 'react' -import { ListItem } from '../../components' -import moment from 'moment' +import { ContextsList } from '../../components' +import { StripHeader } from '../index' import { connect } from 'react-redux' import * as selectors from '../../selectors' import './style.scss' @@ -14,34 +14,11 @@ class PlayQueue extends React.Component { const { contexts, onItemPlayClick, currentItem } = this.props return (
        -
          - {contexts.map((c) => ( -
        • -
          -
          - {c.context.title}
          - {c.items.length} items / - {moment.duration(getDuration(c.items), 's').humanize()} -
          -
          - {c.items.slice(0, 15).map((i, idx)=> ( - - ))} -
          -
          -
            - {c.items.map(i => ( -
          1. - -
          2. - ))} -
          -
        • - ))} -
        + +
        ) } @@ -53,10 +30,6 @@ PlayQueue.propTypes = { currentItem: React.PropTypes.object.isRequired, } -function getDuration(items) { - return items.reduce((r, i) => (r + i.duration), 0) -} - const mapStateToProps = (state) => ({ contexts: selectors.getPlaylistGroupedByContext(state), currentItem: selectors.currentTrack(state), diff --git a/app/l8pr/static/containers/Search/index.js b/app/l8pr/static/containers/Search/index.js new file mode 100644 index 0000000..784ae41 --- /dev/null +++ b/app/l8pr/static/containers/Search/index.js @@ -0,0 +1,33 @@ +import React from 'react' +import { connect } from 'react-redux' +import * as selectors from '../../selectors' +import './style.scss' +import * as player from '../../actions/player' + +class Search extends React.Component { + constructor(props) { + super(props) + } + render() { + return ( +
        + SEACRCH +
        + ) + } +} + +Search.propTypes = { + contexts: React.PropTypes.array.isRequired, + onItemPlayClick: React.PropTypes.func.isRequired, + currentItem: React.PropTypes.object.isRequired, +} + +const mapStateToProps = (state) => ({ + contexts: selectors.getPlaylistGroupedByContext(state), + currentItem: selectors.currentTrack(state), +}) +const mapDispatchToProps = (dispatch) => ({ + onItemPlayClick: (item) => (dispatch(player.playItem(item))), +}) +export default connect(mapStateToProps, mapDispatchToProps)(Search) diff --git a/app/l8pr/static/containers/Search/style.scss b/app/l8pr/static/containers/Search/style.scss new file mode 100644 index 0000000..e69de29 diff --git a/app/l8pr/static/containers/Strip/index.js b/app/l8pr/static/containers/Strip/index.js index a95c9f8..8e164d8 100644 --- a/app/l8pr/static/containers/Strip/index.js +++ b/app/l8pr/static/containers/Strip/index.js @@ -1,7 +1,7 @@ import React from 'react' import { connect } from 'react-redux' import { Progressbar } from '../../components' -import { StripHeader, NavPlayer, PlayQueue } from '../index' +import { StripHeader, NavPlayer, Browser } from '../index' import './style.scss' class Strip extends React.Component { @@ -16,10 +16,7 @@ class Strip extends React.Component { const { stripOpened, progress, loaded, onSeekTo } = this.props return (
        - - { stripOpened && - - } + {stripOpened && }
        diff --git a/app/l8pr/static/containers/StripHeader/index.js b/app/l8pr/static/containers/StripHeader/index.js index 555d949..e0bdd7e 100644 --- a/app/l8pr/static/containers/StripHeader/index.js +++ b/app/l8pr/static/containers/StripHeader/index.js @@ -1,33 +1,36 @@ import React from 'react' import { connect } from 'react-redux' -import * as selectors from '../../selectors' -import { toggleStrip } from '../../actions/browser' +import { browse } from '../../actions/browser' import { showModal } from '../../actions/modal' -import { get } from 'lodash' import './style.scss' -function StripHeaderComponent({ stripOpened, toggleStrip, onLogin }) { +function StripHeaderComponent({ onLogin, onSearch, browserType }) { return (
        + {browserType === 'SEARCH' && 'Search'} + {browserType === 'PLAYQUEUE' && 'Play queue'}
        - - {!stripOpened && 'keyboard_arrow_up'} - {stripOpened && 'keyboard_arrow_down'} - + search account_circle
        ) } +StripHeaderComponent.propTypes = { + onSearch: React.PropTypes.func.isRequired, + browserType: React.PropTypes.string.isRequired, +} + const mapStateToProps = (state) => ({ - stripOpened: state.browser.stripOpened, + browserType: state.browser.browserType, }) const mapDispatchToProps = (dispatch) => ({ - toggleStrip: () => (dispatch(toggleStrip())), + onSearch: () => (dispatch(browse('SEARCH'))), onLogin: () => (dispatch(showModal({ modalType: 'LOGIN' }))), }) + export default connect(mapStateToProps, mapDispatchToProps)(StripHeaderComponent) diff --git a/app/l8pr/static/containers/index.js b/app/l8pr/static/containers/index.js index a018d87..885e532 100644 --- a/app/l8pr/static/containers/index.js +++ b/app/l8pr/static/containers/index.js @@ -4,5 +4,7 @@ export NotFoundView from './NotFound/index' export Strip from './Strip/index' export NavPlayer from './NavPlayer/index' export StripHeader from './StripHeader/index' +export Browser from './Browser/index' +export Search from './Search/index' export { ModalsContainer } from './ModalsContainer' -export PlayQueue from './PlayQueue' +export PlayQueue from './PlayQueue/index' diff --git a/app/l8pr/static/reducers/browser.js b/app/l8pr/static/reducers/browser.js index 82deee4..1d4a9ea 100644 --- a/app/l8pr/static/reducers/browser.js +++ b/app/l8pr/static/reducers/browser.js @@ -1,8 +1,9 @@ -import { RECEIVE_LOOP, SET_STRIP_STATE } from '../constants' +import { BROWSE, SET_STRIP_STATE } from '../constants' export default function (state = { - loop: [], stripOpened: false, + browserType: 'PLAYQUEUE', + browserProps: undefined, }, action = null) { switch (action.type) { case SET_STRIP_STATE: @@ -10,10 +11,11 @@ export default function (state = { ...state, stripOpened: action.payload === true, } - case RECEIVE_LOOP: + case BROWSE: return { ...state, - loop: action.payload, + browserType: action.browserType, + browserProps: action.browserProps, } default: return state From 92d34edae067c36348a13d9021bbd7710b942f9e Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Wed, 19 Apr 2017 09:31:55 +0200 Subject: [PATCH 030/143] animate browser --- .../static/components/ContextsList/index.js | 4 +- .../static/components/ContextsList/style.scss | 3 +- app/l8pr/static/components/ListItem/index.js | 18 +++++--- app/l8pr/static/containers/Browser/index.js | 16 +++++-- app/l8pr/static/containers/Browser/style.scss | 10 +++++ app/l8pr/static/containers/PlayQueue/index.js | 4 +- .../static/containers/PlayQueue/style.scss | 42 +------------------ app/l8pr/static/containers/PlayQueue/test.js | 0 app/l8pr/static/containers/Search/index.js | 2 +- app/l8pr/static/containers/Strip/index.js | 2 +- 10 files changed, 46 insertions(+), 55 deletions(-) create mode 100644 app/l8pr/static/containers/Browser/style.scss delete mode 100644 app/l8pr/static/containers/PlayQueue/test.js diff --git a/app/l8pr/static/components/ContextsList/index.js b/app/l8pr/static/components/ContextsList/index.js index 5a5836b..0a0bebe 100644 --- a/app/l8pr/static/components/ContextsList/index.js +++ b/app/l8pr/static/components/ContextsList/index.js @@ -5,7 +5,7 @@ import './style.scss' export default function ContextsList({ contexts, onItemPlayClick, currentItem }) { return ( -
        +
          {contexts.map((c) => (
        • @@ -41,7 +41,7 @@ export default function ContextsList({ contexts, onItemPlayClick, currentItem }) ContextsList.propTypes = { contexts: React.PropTypes.array.isRequired, onItemPlayClick: React.PropTypes.func.isRequired, - currentItem: React.PropTypes.object.isRequired, + currentItem: React.PropTypes.object, } function getDuration(items) { diff --git a/app/l8pr/static/components/ContextsList/style.scss b/app/l8pr/static/components/ContextsList/style.scss index 2cdec72..ec6c34f 100644 --- a/app/l8pr/static/components/ContextsList/style.scss +++ b/app/l8pr/static/components/ContextsList/style.scss @@ -1,6 +1,7 @@ .ContextsList { - max-height: 55vh; overflow: auto; + background-color: white; + flex-grow: 1; } .context { diff --git a/app/l8pr/static/components/ListItem/index.js b/app/l8pr/static/components/ListItem/index.js index 9a376d2..93af791 100644 --- a/app/l8pr/static/components/ListItem/index.js +++ b/app/l8pr/static/components/ListItem/index.js @@ -2,14 +2,21 @@ import React from 'react' import moment from 'moment' import './style.scss' +const sourceIcones = { + youtube: 'youtube-play', + soundcloud: 'soundcloud', + vimeo: 'vimeo', +} + export default function ListItem({ item, onPlayClick, isPlaying }) { return (
          -
          - {isPlaying && pause} - {item.title} -  {moment.duration(item.duration, 's').humanize()} -
          + {isPlaying && pause} + {item.title} +  {moment.duration(item.duration, 's').humanize()} + +
        + + playlist_play +
        ) @@ -83,6 +87,7 @@ NavPlayer.propTypes = { currentShow: React.PropTypes.object, stripOpened: React.PropTypes.bool, toggleStrip: React.PropTypes.func, + showQueuelist: React.PropTypes.func, } const mapStateToProps = (state) => ({ @@ -103,6 +108,7 @@ const mapDispatchToProps = (dispatch) => ({ onMute: () => (dispatch(player.mute())), onUnmute: () => (dispatch(player.unmute())), toggleStrip: () => (dispatch(browser.toggleStrip())), + showQueuelist: () => (dispatch(browser.browse('PLAYQUEUE'))), }) export default connect(mapStateToProps, mapDispatchToProps)(NavPlayer) diff --git a/app/l8pr/static/containers/PlayQueue/index.js b/app/l8pr/static/containers/PlayQueue/index.js index 013bee6..0fa9885 100644 --- a/app/l8pr/static/containers/PlayQueue/index.js +++ b/app/l8pr/static/containers/PlayQueue/index.js @@ -14,7 +14,7 @@ class PlayQueue extends React.Component { const { contexts, onItemPlayClick, currentItem } = this.props return (
        - + Queue List - SEACRCH + + `Search ${label}`} + multi={true} + onChange={this.handleOnChange.bind(this)} + value={searchTerms} + /> +
        ) } @@ -21,6 +35,7 @@ Search.propTypes = { contexts: React.PropTypes.array.isRequired, onItemPlayClick: React.PropTypes.func.isRequired, currentItem: React.PropTypes.object.isRequired, + searchTerms: React.PropTypes.array, } const mapStateToProps = (state) => ({ diff --git a/app/l8pr/static/containers/StripHeader/index.js b/app/l8pr/static/containers/StripHeader/index.js index 0082df3..9747e17 100644 --- a/app/l8pr/static/containers/StripHeader/index.js +++ b/app/l8pr/static/containers/StripHeader/index.js @@ -4,15 +4,19 @@ import { browse } from '../../actions/browser' import { showModal } from '../../actions/modal' import './style.scss' -function StripHeaderComponent({ onLogin, onSearch, browserType }) { +function StripHeaderComponent({ onLogin, onSearch, browserType, children }) { return (
        - {browserType === 'SEARCH' && 'Search'} - {browserType === 'PLAYQUEUE' && 'Play queue'} + {children}
        - search + { browserType === 'SEARCH' && + close + } + { browserType !== 'SEARCH' && + search + } account_circle
        @@ -21,7 +25,9 @@ function StripHeaderComponent({ onLogin, onSearch, browserType }) { StripHeaderComponent.propTypes = { onSearch: React.PropTypes.func.isRequired, + onLogin: React.PropTypes.func.isRequired, browserType: React.PropTypes.string.isRequired, + children: React.PropTypes.element, } const mapStateToProps = (state) => ({ browserType: state.browser.browserType }) diff --git a/app/l8pr/static/index.js b/app/l8pr/static/index.js index 7937706..a0e29bc 100644 --- a/app/l8pr/static/index.js +++ b/app/l8pr/static/index.js @@ -8,7 +8,6 @@ import configureStore from './store/configureStore' import { authLoginUserSuccess } from './actions/auth' import * as browser from './actions/browser' import * as player from './actions/player' -import * as selectors from './selectors' const initialState = {} const target = document.getElementById('root') @@ -20,11 +19,13 @@ const map = { toggleStrip: 'c', playPause: 'space', toggleMute: 'm', + search: 'ctrl+shift+f', } const handlers = { toggleStrip: () => store.dispatch(browser.toggleStrip()), toggleMute: () => store.dispatch(player.toggleMute()), playPause: () => store.dispatch(player.togglePlay()), + search: () => store.dispatch(browser.browse('SEARCH')), } const node = ( diff --git a/app/l8pr/static/reducers/browser.js b/app/l8pr/static/reducers/browser.js index 1d4a9ea..5fabd28 100644 --- a/app/l8pr/static/reducers/browser.js +++ b/app/l8pr/static/reducers/browser.js @@ -14,6 +14,7 @@ export default function (state = { case BROWSE: return { ...state, + stripOpened: true, browserType: action.browserType, browserProps: action.browserProps, } diff --git a/package.json b/package.json index 72799ea..8580d96 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "react-redux": "4.4.6", "react-router": "3.0.0", "react-router-redux": "4.0.7", + "react-select": "^1.0.0-rc.3", "redux": "3.6.0", "redux-thunk": "2.1.0", "reselect": "^3.0.0", From 574fc74d4b712488848a27fc4f6d2c3ff31d7e7c Mon Sep 17 00:00:00 2001 From: Edouard Richard Date: Sat, 22 Apr 2017 00:14:12 +0200 Subject: [PATCH 036/143] search items --- app/l8pr/api.py | 52 ++++++++++++------- app/l8pr/search_indexes.py | 33 ++++-------- app/l8pr/static/actions/player.js | 9 +++- app/l8pr/static/actions/search.js | 38 ++++++++++++++ app/l8pr/static/components/ListItem/index.js | 45 ++++++++++++++-- app/l8pr/static/constants/index.js | 4 ++ app/l8pr/static/containers/Search/index.js | 38 ++++++++++---- app/l8pr/static/containers/Search/style.scss | 8 +++ app/l8pr/static/reducers/index.js | 2 + app/l8pr/static/reducers/player.js | 5 ++ app/l8pr/static/reducers/search.js | 30 +++++++++++ app/l8pr/static/selectors.js | 6 ++- .../static/utils/{itemsLoaders.js => api.js} | 9 ++++ .../search/indexes/l8pr/item_text.txt | 8 +++ .../search/indexes/l8pr/show_text.txt | 6 +++ requirements.txt | 8 +-- 16 files changed, 235 insertions(+), 66 deletions(-) create mode 100644 app/l8pr/static/actions/search.js create mode 100644 app/l8pr/static/reducers/search.js rename app/l8pr/static/utils/{itemsLoaders.js => api.js} (88%) create mode 100644 app/l8pr/templates/search/indexes/l8pr/item_text.txt create mode 100644 app/l8pr/templates/search/indexes/l8pr/show_text.txt diff --git a/app/l8pr/api.py b/app/l8pr/api.py index ea66957..1064380 100644 --- a/app/l8pr/api.py +++ b/app/l8pr/api.py @@ -5,9 +5,9 @@ from rest_framework.response import Response from django.shortcuts import get_object_or_404 from rest_framework import status -from drf_haystack.serializers import HaystackSerializer +from drf_haystack.serializers import HaystackSerializerMixin, HaystackSerializer from drf_haystack.viewsets import HaystackViewSet -from .search_indexes import ItemIndex, ShowIndex +from .search_indexes import ItemIndex, ShowIndex, UserIndex from .youtube import youtube_search from django.utils import timezone @@ -183,29 +183,41 @@ class ItemViewSet(viewsets.ModelViewSet): filter_fields = ('url', 'users', 'users__username') -class ItemSearchSerializer(HaystackSerializer): - id = serializers.CharField() +class ItemSearchSerializer(HaystackSerializerMixin, ItemSerializer): + class Meta(ItemSerializer.Meta): + search_fields = ('text', 'title',) + field_aliases = {} + exclude = [] + +class ShowSearchSerializer(HaystackSerializerMixin, ShowSerializer): + class Meta(ShowSerializer.Meta): + search_fields = ('text', 'title',) + field_aliases = {} + exclude = [] + + +class UserSearchSerializer(HaystackSerializerMixin, UserSerializer): + class Meta(UserSerializer.Meta): + search_fields = ('text', 'username',) + field_aliases = {} + exclude = [] + + +class AggregateSearchSerializer(HaystackSerializer): class Meta: - # The `index_classes` attribute is a list of which search indexes - # we want to include in the search. - index_classes = [ItemIndex, ShowIndex] - # The `fields` contains all the fields we want to include. - # NOTE: Make sure you don't confuse these with model attributes. These - # fields belong to the search index! - fields = [ - 'title', 'author_name', 'autocomplete', 'thumbnail', 'id', 'url', 'provider_name' - ] + serializers = { + ItemIndex: ItemSearchSerializer, + ShowIndex: ShowSearchSerializer, + UserIndex: UserSearchSerializer, + } class ItemSearchView(HaystackViewSet): - - # `index_models` is an optional list of which models you would like to include - # in the search result. You might have several models indexed, and this provides - # a way to filter out those of no interest for this particular view. - # (Translates to `SearchQuerySet().models(*index_models)` behind the scenes. - index_models = [Item, Show] - serializer_class = ItemSearchSerializer + permission_classes = [] + serializer_class = AggregateSearchSerializer + # to remove in order to have more than Items in results + index_models = [Item] class SearchYoutubeView(views.APIView): diff --git a/app/l8pr/search_indexes.py b/app/l8pr/search_indexes.py index 968bcba..035905e 100644 --- a/app/l8pr/search_indexes.py +++ b/app/l8pr/search_indexes.py @@ -1,45 +1,32 @@ import datetime from haystack import indexes from .models import Item, Show +from django.contrib.auth.models import User class ItemIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True) + text = indexes.CharField(document=True, use_template=True) url = indexes.CharField(model_attr='url') - provider_name = indexes.CharField(model_attr='provider_name') + provider_name = indexes.CharField(model_attr='provider_name', null=True) title = indexes.CharField(model_attr='title') author_name = indexes.CharField(model_attr='author_name', null=True) thumbnail = indexes.CharField(model_attr='thumbnail', null=True) - autocomplete = indexes.EdgeNgramField() def get_model(self): return Item - @staticmethod - def prepare_autocomplete(obj): - return " ".join(( - obj.title, obj.author_name or '' - )) - - def index_queryset(self, using=None): - return self.get_model().objects.filter( - added__lte=datetime.datetime.now() - ) - class ShowIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True) + text = indexes.CharField(document=True, model_attr='title') title = indexes.CharField(model_attr='title') - autocomplete = indexes.EdgeNgramField() def get_model(self): return Show - @staticmethod - def prepare_autocomplete(obj): - return obj.title - def index_queryset(self, using=None): - return self.get_model().objects.filter( - added__lte=datetime.datetime.now() - ) +class UserIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, model_attr='username') + username = indexes.CharField(model_attr='username') + + def get_model(self): + return User diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js index 5f60210..a5d8522 100644 --- a/app/l8pr/static/actions/player.js +++ b/app/l8pr/static/actions/player.js @@ -1,7 +1,7 @@ import * as c from '../constants' import * as selectors from '../selectors' import { push } from 'react-router-redux' -import * as contextLoaders from '../utils/itemsLoaders' +import * as contextLoaders from '../utils/api' export function setPlaylist(playlist) { return { @@ -17,6 +17,13 @@ export function appendToPlaylist(items) { } } +export function insertToPlaylist(items) { + return { + type: c.INSERT_TO_PLAYLIST, + payload: items, + } +} + export function fillQueueList() { return (dispatch, getState) => ( contextLoaders.lastItemsInLoopr() diff --git a/app/l8pr/static/actions/search.js b/app/l8pr/static/actions/search.js new file mode 100644 index 0000000..cc29c12 --- /dev/null +++ b/app/l8pr/static/actions/search.js @@ -0,0 +1,38 @@ +import * as api from '../utils/api' +import * as c from '../constants' + +export function search(searchTerms) { + return (dispatch) => { + dispatch(setTerms(searchTerms)) + if (searchTerms && searchTerms.length) { + api.search(searchTerms) + .then((searchResults) => { + dispatch(setList(searchResults)) + }) + } else { + dispatch(clearList()) + + } + } +} + +function setList(items) { + return { + type: c.SET_SEARCH_RESULT, + payload: items, + } +} + +function clearList(items) { + return { + type: c.CLEAR_SEARCH_LIST, + payload: items, + } +} + +function setTerms(terms) { + return { + type: c.SET_SEARCH_TERMS, + payload: terms, + } +} diff --git a/app/l8pr/static/components/ListItem/index.js b/app/l8pr/static/components/ListItem/index.js index 93af791..a12f820 100644 --- a/app/l8pr/static/components/ListItem/index.js +++ b/app/l8pr/static/components/ListItem/index.js @@ -8,15 +8,50 @@ const sourceIcones = { vimeo: 'vimeo', } -export default function ListItem({ item, onPlayClick, isPlaying }) { + +function getItemType(item) { + if (item.url) { + return Track + } else if (item.items) { + return Show + } else if (item.loops) { + return User + } +} + +export default function ListItem(props) { + const SpecificItem = getItemType(props.item) + return +} + +function User({ item, onPlayClick, isPlaying }) { + return ( +
        + {item.username} +
        + ) +} +function Show({ item, onPlayClick, isPlaying }) { + return ( +
        +
        {item.title} by {item.user.username}
        +
        {item.items.slice(0, 3).map((i) => ( + + ))}
        +
        + ) +} +function Track({ item, onPlayClick, isPlaying }) { return ( -
        +
        {isPlaying && pause} {item.title}  {moment.duration(item.duration, 's').humanize()} - -