Commit dbd8ca9a authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg

Merge branch 'feature/photos-search' into 'master'

Add working 'next' processing and add search for albums

Closes #105

See merge request !304
parents 0f07f51f 0f75b341
......@@ -7,6 +7,11 @@ Object {
"Dismiss": "Sluiten",
"Open": "Openen",
},
"sagas/profile": Object {
"Change profile picture": "Profielafbeelding veranderen",
"Could not update profile picture": "De profielafbeelding kon niet worden bijgewerkt",
"Uploading your new profile picture...": "Nieuwe profielafbeelding uploaden...",
},
"sagas/registration": Object {
"Registration successful!": "Registratie succesvol!",
"Successfully cancelled registration": "Afmelding succesvol",
......@@ -27,13 +32,16 @@ Object {
"Logout": "Uitloggen",
"Member List": "Ledenlijst",
"No": "Nee",
"Photos": "Foto's",
"Settings": "Instellingen",
"Welcome": "Welkom",
"Yes": "Ja",
},
"ui/components/standardHeader/StandardHeader": Object {
"Album": "Album",
"Calendar": "Agenda",
"Event": "Evenement",
"Photos": "Foto's",
"Pizza": "Pizza",
"Profile": "Profiel",
"Registration": "Registratie",
......@@ -105,6 +113,7 @@ Object {
"Sorry! We couldn't load any data.": "Sorry! We konden geen gegevens laden.",
"This field is required and must be an integer.": "Dit veld is verplicht en moet een getal zijn.",
"This field is required.": "Dit veld is verplicht.",
"This field must be an integer.": "Dit veld moet een cijfer zijn.",
},
"ui/screens/login/LoginScreen": Object {
"Become a member": "Lid worden",
......@@ -119,6 +128,15 @@ Object {
"Member List": "Ledenlijst",
"Sorry! We couldn't load any data.": "Sorry, we konden geen gegevens laden.",
},
"ui/screens/photos/AlbumDetailScreen": Object {
"Sorry! We couldn't load any data.": "Sorry, we konden geen gegevens laden.",
},
"ui/screens/photos/AlbumsOverviewScreen": Object {
"Couldn't find any albums...": "Er zijn geen albums gevonden...",
"Find an album": "Vind een album",
"Photos": "Foto's",
"Sorry! We couldn't load any data.": "Sorry, we konden geen gegevens laden.",
},
"ui/screens/pizza/PizzaScreen": Object {
"Changing your order": "Bestelling bewerken",
"Current order": "Huidige bestelling",
......
export const PHOTOS_ALBUMS_OPEN = 'PHOTOS_ALBUMS_OPEN';
export const PHOTOS_ALBUMS_LOAD = 'PHOTOS_ALBUMS_LOAD';
export const PHOTOS_ALBUMS_SUCCESS = 'PHOTOS_ALBUMS_SUCCESS';
export const PHOTOS_ALBUMS_FAILURE = 'PHOTOS_ALBUMS_FAILURE';
export const PHOTOS_ALBUMS_FETCHING = 'PHOTOS_ALBUMS_FETCHING';
......@@ -6,7 +7,6 @@ export const PHOTOS_ALBUM_OPEN = 'PHOTOS_ALBUM_OPEN';
export const PHOTOS_ALBUM_SUCCESS = 'PHOTOS_ALBUM_SUCCESS';
export const PHOTOS_ALBUM_FAILURE = 'PHOTOS_ALBUM_FAILURE';
export const PHOTOS_ALBUM_FETCHING = 'PHOTOS_ALBUM_FETCHING';
export const PHOTOS_PHOTO_SHOW = 'PHOTOS_PHOTO_SHOW';
export const PHOTOS_GALLERY_OPEN = 'PHOTOS_GALLERY_OPEN';
export function openAlbums() {
......@@ -15,10 +15,17 @@ export function openAlbums() {
};
}
export function successAlbums(albums, more) {
export function loadAlbums(keywords, next) {
return {
type: PHOTOS_ALBUMS_LOAD,
payload: { keywords, next },
};
}
export function successAlbums(data, next, isNext) {
return {
type: PHOTOS_ALBUMS_SUCCESS,
payload: { albums, more },
payload: { data, next, isNext },
};
}
......@@ -53,10 +60,10 @@ export function openAlbumWithSlug(slug) {
};
}
export function successAlbum(album) {
export function successAlbum(data) {
return {
type: PHOTOS_ALBUM_SUCCESS,
payload: album,
payload: { data },
};
}
......@@ -69,10 +76,3 @@ export function fetchingAlbum() {
type: PHOTOS_ALBUM_FETCHING,
};
}
export function showPhoto(photo) {
return {
type: PHOTOS_PHOTO_SHOW,
payload: photo,
};
}
......@@ -12,11 +12,14 @@ files['app/ui/screens/events/RegistrationScreenNL'] = require('./nl/app/ui/scree
files['app/ui/screens/events/EventAdminScreenNL'] = require('./nl/app/ui/screens/events/EventAdminScreen.json');
files['app/ui/screens/events/CalendarScreenNL'] = require('./nl/app/ui/screens/events/CalendarScreen.json');
files['app/ui/screens/events/EventScreenNL'] = require('./nl/app/ui/screens/events/EventScreen.json');
files['app/ui/screens/photos/AlbumsOverviewScreenNL'] = require('./nl/app/ui/screens/photos/AlbumsOverviewScreen.json');
files['app/ui/screens/photos/AlbumDetailScreenNL'] = require('./nl/app/ui/screens/photos/AlbumDetailScreen.json');
files['app/ui/screens/pizza/PizzaScreenNL'] = require('./nl/app/ui/screens/pizza/PizzaScreen.json');
files['app/ui/screens/login/LoginScreenNL'] = require('./nl/app/ui/screens/login/LoginScreen.json');
files['app/ui/components/sidebar/SidebarNL'] = require('./nl/app/ui/components/sidebar/Sidebar.json');
files['app/ui/components/standardHeader/StandardHeaderNL'] = require('./nl/app/ui/components/standardHeader/StandardHeader.json');
files['app/ui/components/errorScreen/ErrorScreenNL'] = require('./nl/app/ui/components/errorScreen/ErrorScreen.json');
files['app/sagas/profileNL'] = require('./nl/app/sagas/profile.json');
files['app/sagas/sessionNL'] = require('./nl/app/sagas/session.json');
files['app/sagas/registrationNL'] = require('./nl/app/sagas/registration.json');
files['app/appNL'] = require('./nl/app/app.json');
......@@ -36,11 +39,14 @@ export default {
'ui/screens/events/EventAdminScreen': files['app/ui/screens/events/EventAdminScreenNL'],
'ui/screens/events/CalendarScreen': files['app/ui/screens/events/CalendarScreenNL'],
'ui/screens/events/EventScreen': files['app/ui/screens/events/EventScreenNL'],
'ui/screens/photos/AlbumsOverviewScreen': files['app/ui/screens/photos/AlbumsOverviewScreenNL'],
'ui/screens/photos/AlbumDetailScreen': files['app/ui/screens/photos/AlbumDetailScreenNL'],
'ui/screens/pizza/PizzaScreen': files['app/ui/screens/pizza/PizzaScreenNL'],
'ui/screens/login/LoginScreen': files['app/ui/screens/login/LoginScreenNL'],
'ui/components/sidebar/Sidebar': files['app/ui/components/sidebar/SidebarNL'],
'ui/components/standardHeader/StandardHeader': files['app/ui/components/standardHeader/StandardHeaderNL'],
'ui/components/errorScreen/ErrorScreen': files['app/ui/components/errorScreen/ErrorScreenNL'],
'sagas/profile': files['app/sagas/profileNL'],
'sagas/session': files['app/sagas/sessionNL'],
'sagas/registration': files['app/sagas/registrationNL'],
app: files['app/appNL'],
......
{
"Change profile picture": "Profielafbeelding veranderen",
"Uploading your new profile picture...": "Nieuwe profielafbeelding uploaden...",
"Could not update profile picture": "De profielafbeelding kon niet worden bijgewerkt"
}
......@@ -7,5 +7,6 @@
"Calendar": "Agenda",
"Logout": "Uitloggen",
"Member List": "Ledenlijst",
"Settings": "Instellingen"
"Settings": "Instellingen",
"Photos": "Foto's"
}
......@@ -6,5 +6,7 @@
"Profile": "Profiel",
"Registration": "Registratie",
"Settings": "Instellingen",
"Registrations": "Inschrijvingen"
"Registrations": "Inschrijvingen",
"Photos": "Foto's",
"Album": "Album"
}
......@@ -2,5 +2,6 @@
"This field is required and must be an integer.": "Dit veld is verplicht en moet een getal zijn.",
"This field is required.": "Dit veld is verplicht.",
"Sorry! We couldn't load any data.": "Sorry! We konden geen gegevens laden.",
"Save": "Aanpassen"
"Save": "Aanpassen",
"This field must be an integer.": "Dit veld moet een cijfer zijn."
}
{
"Sorry! We couldn't load any data.": "Sorry, we konden geen gegevens laden."
}
{
"Photos": "Foto's",
"Find an album": "Vind een album",
"Sorry! We couldn't load any data.": "Sorry, we konden geen gegevens laden.",
"Couldn't find any albums...": "Er zijn geen albums gevonden..."
}
......@@ -6,6 +6,7 @@ export const STATUS_FAILURE = 'failure';
const initialState = {
albums: {
keywords: undefined,
status: STATUS_INITIAL,
fetching: true,
data: [],
......@@ -18,27 +19,40 @@ const initialState = {
},
};
export default function photos(state = initialState, action = {}) {
switch (action.type) {
export default function photos(state = initialState, { type, payload }) {
switch (type) {
case photosActions.PHOTOS_ALBUMS_FETCHING:
return {
...state,
albums: initialState.albums,
albums: {
...state.albums,
fetching: true,
},
};
case photosActions.PHOTOS_ALBUMS_LOAD:
return {
...state,
albums: {
...state.albums,
keywords: payload.keywords,
},
};
case photosActions.PHOTOS_ALBUMS_SUCCESS:
return {
...state,
albums: {
...state.albums,
status: STATUS_SUCCESS,
fetching: false,
data: action.payload.albums,
more: action.payload.more,
data: payload.isNext ? [...state.albums.data, ...payload.data] : payload.data,
next: payload.next,
},
};
case photosActions.PHOTOS_ALBUMS_FAILURE:
return {
...state,
albums: {
...state.albums,
status: STATUS_FAILURE,
fetching: false,
data: [],
......@@ -55,7 +69,7 @@ export default function photos(state = initialState, action = {}) {
album: {
status: STATUS_SUCCESS,
fetching: false,
data: action.payload,
data: payload.data,
selection: undefined,
},
};
......@@ -74,7 +88,7 @@ export default function photos(state = initialState, action = {}) {
...state,
album: {
...state.album,
selection: action.payload.selection,
selection: payload.selection,
},
};
default:
......
......@@ -5,7 +5,7 @@ import {
import { apiRequest, tokenSelector } from '../utils/url';
import * as photosActions from '../actions/photos';
function* loadAlbums() {
function* loadAlbums({ payload: { keywords, next } }) {
const token = yield select(tokenSelector);
yield put(photosActions.fetchingAlbums());
......@@ -23,11 +23,21 @@ function* loadAlbums() {
limit: 12,
};
if (keywords) {
params.search = keywords;
}
try {
const response = yield call(apiRequest, 'photos/albums', data, params);
let response;
if (next) {
response = yield call(apiRequest, next, data);
} else {
response = yield call(apiRequest, 'photos/albums', data, params);
}
yield put(photosActions.successAlbums(
response.results.filter(item => item.accessible && !item.hidden && item.cover != null),
response.next,
next !== undefined,
));
} catch (error) {
yield put(photosActions.failureAlbums());
......@@ -72,5 +82,6 @@ function* loadAlbum(action) {
export default function* photosSaga() {
yield takeEvery(photosActions.PHOTOS_ALBUMS_OPEN, loadAlbums);
yield takeEvery(photosActions.PHOTOS_ALBUMS_LOAD, loadAlbums);
yield takeEvery(photosActions.PHOTOS_ALBUM_OPEN, loadAlbum);
}
export const photosStore = state => state.photos;
export const albumsData = state => photosStore(state).albums.data;
export const albumsKeywords = state => photosStore(state).albums.keywords;
export const albumsNext = state => photosStore(state).albums.next;
export const albumsStatus = state => photosStore(state).albums.status;
export const isFetchingAlbums = state => photosStore(state).albums.fetching;
......
......@@ -153,6 +153,7 @@ class SearchHeader extends Component {
}
SearchHeader.defaultProps = {
searchKey: '',
leftIcon: 'menu',
};
......@@ -160,7 +161,7 @@ SearchHeader.propTypes = {
title: PropTypes.string.isRequired,
searchText: PropTypes.string.isRequired,
search: PropTypes.func.isRequired,
searchKey: PropTypes.string.isRequired,
searchKey: PropTypes.string,
leftIcon: PropTypes.string,
leftIconAction: PropTypes.func.isRequired,
};
......
import React from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Dimensions, FlatList, View } from 'react-native';
import { STATUS_FAILURE, STATUS_INITIAL } from '../../../reducers/photos';
......@@ -6,33 +6,54 @@ import LoadingScreen from '../../components/loadingScreen/LoadingScreen';
import ErrorScreen from '../../components/errorScreen/ErrorScreen';
import AlbumListItem from './AlbumListItem';
import styles from './style/AlbumsOverviewScreen';
import { withStandardHeader } from '../../components/standardHeader/StandardHeader';
import SearchHeader from '../../components/searchHeader/SearchHeaderConnector';
import DismissKeyboardView from '../../components/dismissKeyboardView/DismissKeyboardView';
const windowWidth = Dimensions.get('window').width;
const numColumns = 2;
export const albumSize = (windowWidth - 48) / numColumns;
const AlbumsOverviewScreen = (props) => {
const {
t, fetching, status, openAlbum,
} = props;
if (fetching && status === STATUS_INITIAL) {
return <LoadingScreen />;
}
if (!fetching && status === STATUS_FAILURE) {
return (
<View style={styles.wrapper}>
<ErrorScreen style={styles.errorScreen} message={t('Sorry! We couldn\'t load any data.')} />
</View>
class AlbumsOverviewScreen extends Component {
handleRefresh = () => {
const { keywords, loadAlbums } = this.props;
loadAlbums(keywords);
};
handleEndReached = () => {
const {
keywords, next, loadAlbums, fetching,
} = this.props;
if (!fetching && next !== null) {
loadAlbums(keywords, next);
}
};
search = (keywords) => {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.props.loadAlbums(keywords);
}, 500);
};
render() {
const {
t, fetching, status, openAlbum, albums, keywords,
} = this.props;
const header = (
<SearchHeader
title={t('Photos')}
searchText={t('Find an album')}
search={this.search}
searchKey={keywords}
/>
);
}
return (
<View style={styles.wrapper}>
let content = (
<FlatList
style={styles.flatList}
contentContainerStyle={styles.listContainer}
data={props.albums}
data={albums}
renderItem={
({ item }) => (
<AlbumListItem
......@@ -45,18 +66,46 @@ const AlbumsOverviewScreen = (props) => {
}
keyExtractor={item => item.pk}
numColumns={numColumns}
onRefresh={this.handleRefresh}
refreshing={fetching}
onEndReachedThreshold={0.5}
onEndReached={this.handleEndReached}
/>
</View>
);
};
);
if (fetching && status === STATUS_INITIAL) {
content = (<LoadingScreen />);
} else if (!fetching && status === STATUS_FAILURE) {
content = (<ErrorScreen message={t('Sorry! We couldn\'t load any data.')} />);
} else if (albums.length === 0) {
content = (<ErrorScreen message={t('Couldn\'t find any albums...')} />);
}
return (
<View style={styles.wrapper}>
{header}
<DismissKeyboardView contentStyle={styles.keyboardView}>
{content}
</DismissKeyboardView>
</View>
);
}
}
AlbumsOverviewScreen.defaultProps = {
next: null,
keywords: undefined,
};
AlbumsOverviewScreen.propTypes = {
fetching: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
keywords: PropTypes.string,
albums: PropTypes.arrayOf(AlbumListItem.propTypes.album).isRequired,
openAlbum: PropTypes.func.isRequired,
loadAlbums: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
next: PropTypes.string,
};
export default withStandardHeader(AlbumsOverviewScreen, true);
export default AlbumsOverviewScreen;
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import AlbumsOverview from './AlbumsOverviewScreen';
import { albumsData, albumsStatus, isFetchingAlbums } from '../../../selectors/photos';
import {
albumsData, albumsStatus, isFetchingAlbums, albumsKeywords, albumsNext,
} from '../../../selectors/photos';
import * as photosActions from '../../../actions/photos';
const mapStateToProps = state => ({
albums: albumsData(state),
keywords: albumsKeywords(state),
status: albumsStatus(state),
fetching: isFetchingAlbums(state),
next: albumsNext(state),
});
const mapDispatchToProps = {
openAlbum: photosActions.openAlbum,
loadAlbums: photosActions.loadAlbums,
};
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation(['screens/photos/AlbumsOverview'])(AlbumsOverview));
......@@ -23,4 +23,7 @@ export default StyleSheet.create({
errorScreen: {
flex: 1,
},
keyboardView: {
flex: 1,
},
});
......@@ -99,7 +99,11 @@ indexStream.once('open', () => {
const langName = lang.toUpperCase();
files.forEach((fileName) => {
const ns = fileName.substring(4);
indexStream.write(` '${ns}': files['${fileName}${langName}'],\n`);
if (ns.indexOf('/') > -1) {
indexStream.write(` '${ns}': files['${fileName}${langName}'],\n`);
} else {
indexStream.write(` ${ns}: files['${fileName}${langName}'],\n`);
}
});
indexStream.write(' },\n');
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment