mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 03:43:28 +10:00
commit
cf035c3368
16 changed files with 471 additions and 15 deletions
3
src/containers/EditListingForm/EditListingForm.css
Normal file
3
src/containers/EditListingForm/EditListingForm.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.error {
|
||||
color: red;
|
||||
}
|
||||
11
src/containers/EditListingForm/EditListingForm.example.js
Normal file
11
src/containers/EditListingForm/EditListingForm.example.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
import EditListingForm from './EditListingForm';
|
||||
|
||||
export const Empty = {
|
||||
component: EditListingForm,
|
||||
props: {
|
||||
onSubmit(values) {
|
||||
console.log('submit new password form values:', values);
|
||||
},
|
||||
},
|
||||
};
|
||||
92
src/containers/EditListingForm/EditListingForm.js
Normal file
92
src/containers/EditListingForm/EditListingForm.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
|
||||
import { intlShape, injectIntl } from 'react-intl';
|
||||
import { maxLength, required } from '../../util/validators';
|
||||
import css from './EditListingForm.css';
|
||||
|
||||
const MAX_LENGTH = 60;
|
||||
|
||||
const RenderField = ({ input, label, type, meta: { touched, error } }) => {
|
||||
const component = type === 'textarea'
|
||||
? <textarea {...input} placeholder={label} />
|
||||
: <input {...input} placeholder={label} type={type} />;
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={input.name}>{label}</label>
|
||||
<div>
|
||||
{component}
|
||||
{touched && (error && <span className={css.error}>{error}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
const { any, bool, shape, string } = PropTypes;
|
||||
|
||||
RenderField.propTypes = {
|
||||
input: any.isRequired,
|
||||
label: string.isRequired,
|
||||
type: string.isRequired,
|
||||
meta: shape({
|
||||
touched: bool,
|
||||
error: string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
class EditListingForm extends Component {
|
||||
componentDidMount() {
|
||||
this.handleInitialize();
|
||||
}
|
||||
|
||||
handleInitialize() {
|
||||
const { initData = {}, initialize } = this.props;
|
||||
initialize(initData);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
disabled,
|
||||
handleSubmit,
|
||||
intl,
|
||||
pristine,
|
||||
saveActionMsg = 'Save listing',
|
||||
submitting,
|
||||
} = this.props;
|
||||
const requiredStr = intl.formatMessage({ id: 'EditListingForm.required' });
|
||||
const maxLengthStr = intl.formatMessage({ id: 'EditListingForm.maxLength' }, {
|
||||
maxLength: MAX_LENGTH,
|
||||
});
|
||||
const maxLength60 = maxLength(maxLengthStr, MAX_LENGTH);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Field
|
||||
name="title"
|
||||
label="Title"
|
||||
component={RenderField}
|
||||
type="text"
|
||||
validate={[required(requiredStr), maxLength60]}
|
||||
/>
|
||||
|
||||
<Field
|
||||
name="description"
|
||||
label="Description"
|
||||
component={RenderField}
|
||||
type="textarea"
|
||||
validate={[required(requiredStr)]}
|
||||
/>
|
||||
<button type="submit" disabled={pristine || submitting || disabled}>{saveActionMsg}</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditListingForm.defaultProps = { initData: {} };
|
||||
|
||||
EditListingForm.propTypes = {
|
||||
...formPropTypes,
|
||||
initData: shape({ title: string, description: string }),
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default reduxForm({ form: 'EditListingForm' })(injectIntl(EditListingForm));
|
||||
10
src/containers/EditListingForm/EditListingForm.test.js
Normal file
10
src/containers/EditListingForm/EditListingForm.test.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import { renderDeep } from '../../util/test-helpers';
|
||||
import EditListingForm from './EditListingForm';
|
||||
|
||||
describe('EditListingForm', () => {
|
||||
it('matches snapshot', () => {
|
||||
const tree = renderDeep(<EditListingForm />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
exports[`EditListingForm matches snapshot 1`] = `
|
||||
<form
|
||||
onSubmit={[Function]}>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="title">
|
||||
Title
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
name="title"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onDragStart={[Function]}
|
||||
onDrop={[Function]}
|
||||
onFocus={[Function]}
|
||||
placeholder="Title"
|
||||
type="text"
|
||||
value="" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="description">
|
||||
Description
|
||||
</label>
|
||||
<div>
|
||||
<textarea
|
||||
name="description"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onDragStart={[Function]}
|
||||
onDrop={[Function]}
|
||||
onFocus={[Function]}
|
||||
placeholder="Description"
|
||||
value="" />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
disabled={true}
|
||||
type="submit">
|
||||
Save listing
|
||||
</button>
|
||||
</form>
|
||||
`;
|
||||
131
src/containers/EditListingPage/EditListingPage.duck.js
Normal file
131
src/containers/EditListingPage/EditListingPage.duck.js
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/* eslint-disable no-console */
|
||||
import { showListingsSuccess as globalShowListingsSuccess } from '../../ducks/sdk.duck';
|
||||
|
||||
const requestAction = actionType => params => ({ type: actionType, payload: { params } });
|
||||
|
||||
const successAction = actionType => result => ({ type: actionType, payload: result.data });
|
||||
|
||||
const errorAction = actionType => error => ({ type: actionType, payload: error, error: true });
|
||||
|
||||
// ================ Action types ================ //
|
||||
|
||||
export const CREATE_LISTING_REQUEST = 'app/EditListingPage/CREATE_LISTING_REQUEST';
|
||||
export const CREATE_LISTING_SUCCESS = 'app/EditListingPage/CREATE_LISTING_SUCCESS';
|
||||
export const CREATE_LISTING_ERROR = 'app/EditListingPage/CREATE_LISTING_ERROR';
|
||||
|
||||
export const SHOW_LISTINGS_REQUEST = 'app/EditListingPage/SHOW_LISTINGS_REQUEST';
|
||||
export const SHOW_LISTINGS_SUCCESS = 'app/EditListingPage/SHOW_LISTINGS_SUCCESS';
|
||||
export const SHOW_LISTINGS_ERROR = 'app/EditListingPage/SHOW_LISTINGS_ERROR';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
// Error instance placeholders for each endpoint
|
||||
createListingsError: null,
|
||||
showListingsError: null,
|
||||
submittedListingId: null,
|
||||
redirectToListing: false,
|
||||
};
|
||||
|
||||
export default function reducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case CREATE_LISTING_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
createListingsError: null,
|
||||
submittedListingId: null,
|
||||
redirectToListing: false,
|
||||
};
|
||||
case CREATE_LISTING_SUCCESS:
|
||||
return { ...state, submittedListingId: payload.data.id, redirectToListing: true };
|
||||
case CREATE_LISTING_ERROR:
|
||||
console.error(payload);
|
||||
return { ...state, createListingsError: payload, redirectToListing: false };
|
||||
|
||||
case SHOW_LISTINGS_REQUEST:
|
||||
return { ...state, showListingsError: null };
|
||||
case SHOW_LISTINGS_SUCCESS:
|
||||
return initialState;
|
||||
case SHOW_LISTINGS_ERROR:
|
||||
console.error(payload);
|
||||
return { ...state, showListingsError: payload, redirectToListing: false };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// ================ Selectors ================ //
|
||||
|
||||
// ================ Action creators ================ //
|
||||
|
||||
// All the action creators that don't have the {Success, Error} suffix
|
||||
// take the params object that the corresponding SDK endpoint method
|
||||
// expects.
|
||||
|
||||
// SDK method: listings.create
|
||||
export const createListing = requestAction(CREATE_LISTING_REQUEST);
|
||||
export const createListingSuccess = successAction(CREATE_LISTING_SUCCESS);
|
||||
export const createListingError = errorAction(CREATE_LISTING_ERROR);
|
||||
|
||||
// SDK method: listings.show
|
||||
export const showListings = requestAction(SHOW_LISTINGS_REQUEST);
|
||||
export const showListingsSuccess = successAction(SHOW_LISTINGS_SUCCESS);
|
||||
export const showListingsError = errorAction(SHOW_LISTINGS_ERROR);
|
||||
|
||||
// ================ Thunk ================ //
|
||||
|
||||
const endpoints = {
|
||||
[CREATE_LISTING_REQUEST]: {
|
||||
method: sdk => sdk.listings.create,
|
||||
success: createListingSuccess,
|
||||
error: createListingError,
|
||||
},
|
||||
[SHOW_LISTINGS_REQUEST]: {
|
||||
method: sdk => sdk.listings.show,
|
||||
success: showListingsSuccess,
|
||||
error: showListingsError,
|
||||
},
|
||||
};
|
||||
|
||||
function callEndpoint(sdkMethod, dispatchSuccess, dispatchError, action) {
|
||||
const params = action.payload.params;
|
||||
return sdkMethod(params).then(resp => dispatchSuccess(resp)).catch(e => dispatchError(e));
|
||||
}
|
||||
|
||||
export function requestCreateListing(actionPayload) {
|
||||
return (dispatch, getState, sdk) => {
|
||||
// Create request action
|
||||
const action = createListing(actionPayload);
|
||||
// Notify store that we are sending a request
|
||||
dispatch(action);
|
||||
// Select SDK endpoint and call it
|
||||
const { method, success, error } = endpoints[action.type];
|
||||
return callEndpoint(
|
||||
method(sdk),
|
||||
response => dispatch(success(response)),
|
||||
e => dispatch(error(e)),
|
||||
action,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function requestShowListing(actionPayload) {
|
||||
return (dispatch, getState, sdk) => {
|
||||
const action = showListings(actionPayload);
|
||||
dispatch(action);
|
||||
const { method, success, error } = endpoints[action.type];
|
||||
return callEndpoint(
|
||||
method(sdk),
|
||||
response => {
|
||||
// EditListingPage fetches new listing data, which also needs to be added to global data
|
||||
dispatch(globalShowListingsSuccess(response));
|
||||
// In case of success, we'll clear state.EditListingPage (user will be redirected away)
|
||||
dispatch(success(response));
|
||||
},
|
||||
e => dispatch(error(e)),
|
||||
action,
|
||||
);
|
||||
};
|
||||
}
|
||||
117
src/containers/EditListingPage/EditListingPage.js
Normal file
117
src/containers/EditListingPage/EditListingPage.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { intlShape, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { types } from 'sharetribe-sdk';
|
||||
import { NamedRedirect, PageLayout } from '../../components';
|
||||
import { EditListingForm } from '../../containers';
|
||||
import { getListingsById } from '../../ducks/sdk.duck';
|
||||
import { createSlug } from '../../util/urlHelpers';
|
||||
import { requestCreateListing, requestShowListing } from './EditListingPage.duck';
|
||||
|
||||
const formatRequestData = values => {
|
||||
const { title, description } = values;
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
address: 'Bulevardi 14, 00200 Helsinki, Finland',
|
||||
geolocation: {
|
||||
lat: 40.6,
|
||||
lng: 73.9,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const onSubmit = (submitListing, onLoadListing) => {
|
||||
return values =>
|
||||
submitListing(values).then(resp => onLoadListing(resp.payload.data.id.uuid)).catch(e => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
// N.B. All the presentational content needs to be extracted to their own components
|
||||
export class EditListingPageComponent extends Component {
|
||||
componentDidMount() {
|
||||
if (this.props.type === 'edit') {
|
||||
EditListingPageComponent.loadData(this.props.params.id, this.props.onLoadListing);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, intl, onCreateListing, onLoadListing, page, params, type } = this.props;
|
||||
const isNew = type === 'new';
|
||||
const id = page.submittedListingId || new types.UUID(params.id);
|
||||
const listingsById = getListingsById(data, [id]);
|
||||
const currentListing = listingsById.length > 0 ? listingsById[0] : null;
|
||||
|
||||
const shouldRedirect = page.submittedListingId && currentListing;
|
||||
const showForm = isNew || currentListing;
|
||||
|
||||
if (shouldRedirect) {
|
||||
const slug = currentListing ? createSlug(currentListing.attributes.title) : null;
|
||||
return <NamedRedirect name="ListingPage" params={{ id: id.uuid, slug }} />;
|
||||
} else if (showForm) {
|
||||
const saveActionMsg = page.redirectToListing ? 'Redirecting to listing' : 'Create listing';
|
||||
const disableForm = page.redirectToListing && !page.showListingsError;
|
||||
|
||||
// Currently creates a new listing based on existing one (sdk doesn't support listings.update)
|
||||
const initData = currentListing && type === 'edit'
|
||||
? {
|
||||
title: currentListing.attributes.title,
|
||||
description: currentListing.attributes.description,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<PageLayout title={'Edit listing'}>
|
||||
<EditListingForm
|
||||
onSubmit={onSubmit(onCreateListing, onLoadListing)}
|
||||
saveActionMsg={saveActionMsg}
|
||||
disabled={disableForm}
|
||||
initData={initData}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
} else {
|
||||
const loadingPageMsg = {
|
||||
id: 'ListingPage.loadingListingData',
|
||||
};
|
||||
return <PageLayout title={intl.formatMessage(loadingPageMsg)} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditListingPageComponent.loadData = (id, onLoadListing) => {
|
||||
onLoadListing(id);
|
||||
};
|
||||
|
||||
EditListingPageComponent.defaultProps = { listing: null, params: null, type: 'edit' };
|
||||
|
||||
const { func, object, shape, string } = PropTypes;
|
||||
|
||||
EditListingPageComponent.propTypes = {
|
||||
data: object.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
onLoadListing: func.isRequired,
|
||||
onCreateListing: func.isRequired,
|
||||
page: object.isRequired,
|
||||
params: shape({
|
||||
id: string,
|
||||
slug: string,
|
||||
}),
|
||||
type: string,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { data = {}, EditListingPage } = state;
|
||||
return { page: EditListingPage, data };
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onLoadListing: id => dispatch(requestShowListing({ id, include: ['author', 'images'] })),
|
||||
onCreateListing: values => dispatch(requestCreateListing(formatRequestData(values))),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(EditListingPageComponent));
|
||||
|
|
@ -64,26 +64,28 @@ export class ListingPageComponent extends Component {
|
|||
: [];
|
||||
|
||||
// TODO componentize
|
||||
const imageCarousel = images.length > 0
|
||||
? (
|
||||
<div className={css.imageContainer}>
|
||||
<img className={css.mainImage} alt={title} src={images[0].sizes[0].url} />
|
||||
<div className={css.thumbnailContainer}>
|
||||
{images.slice(1).map(image => (
|
||||
<div key={image.id.uuid} className={css.squareWrapper}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<img className={css.thumbnail} alt={`${title} thumbnail`} src={image.sizes[0].url} />
|
||||
</div>
|
||||
const imageCarousel = images.length > 0
|
||||
? <div className={css.imageContainer}>
|
||||
<img className={css.mainImage} alt={title} src={images[0].sizes[0].url} />
|
||||
<div className={css.thumbnailContainer}>
|
||||
{images.slice(1).map(image => (
|
||||
<div key={image.id.uuid} className={css.squareWrapper}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<img
|
||||
className={css.thumbnail}
|
||||
alt={`${title} thumbnail`}
|
||||
src={image.sizes[0].url}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
: null;
|
||||
|
||||
const pageContent = (
|
||||
<PageLayout title={`${title} ${info.price}`}>
|
||||
{ imageCarousel }
|
||||
{imageCarousel}
|
||||
{/* eslint-disable react/no-danger */}
|
||||
<div className={css.description} dangerouslySetInnerHTML={{ __html: description }} />
|
||||
{/* eslint-enable react/no-danger */}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import ChangeAccountPasswordForm from './ChangeAccountPasswordForm/ChangeAccount
|
|||
import ChangePasswordForm from './ChangePasswordForm/ChangePasswordForm';
|
||||
import CheckoutPage from './CheckoutPage/CheckoutPage';
|
||||
import ContactDetailsPage from './ContactDetailsPage/ContactDetailsPage';
|
||||
import EditListingForm from './EditListingForm/EditListingForm';
|
||||
import EditListingPage from './EditListingPage/EditListingPage';
|
||||
import EditProfilePage from './EditProfilePage/EditProfilePage';
|
||||
import HeroSearchForm from './HeroSearchForm/HeroSearchForm';
|
||||
import InboxPage from './InboxPage/InboxPage';
|
||||
|
|
@ -30,6 +32,8 @@ export {
|
|||
ChangePasswordForm,
|
||||
CheckoutPage,
|
||||
ContactDetailsPage,
|
||||
EditListingForm,
|
||||
EditListingPage,
|
||||
EditProfilePage,
|
||||
HeroSearchForm,
|
||||
InboxPage,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import EditListingPage from './EditListingPage/EditListingPage.duck';
|
||||
import SearchPage from './SearchPage/SearchPage.duck';
|
||||
|
||||
export { SearchPage };
|
||||
export { EditListingPage, SearchPage };
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import * as NamedLink from './components/NamedLink/NamedLink.example';
|
|||
import * as ChangeAccountPasswordForm
|
||||
from './containers/ChangeAccountPasswordForm/ChangeAccountPasswordForm.example';
|
||||
import * as ChangePasswordForm from './containers/ChangePasswordForm/ChangePasswordForm.example';
|
||||
import * as EditListingForm from './containers/EditListingForm/EditListingForm.example';
|
||||
import * as HeroSearchForm from './containers/HeroSearchForm/HeroSearchForm.example';
|
||||
import * as LoginForm from './containers/LoginForm/LoginForm.example';
|
||||
import * as PasswordForgottenForm
|
||||
|
|
@ -17,6 +18,7 @@ export {
|
|||
BookingInfo,
|
||||
ChangeAccountPasswordForm,
|
||||
ChangePasswordForm,
|
||||
EditListingForm,
|
||||
HeroSearchForm,
|
||||
ListingCard,
|
||||
LoginForm,
|
||||
|
|
|
|||
|
|
@ -49,3 +49,8 @@ button {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
AuthenticationPage,
|
||||
CheckoutPage,
|
||||
ContactDetailsPage,
|
||||
EditListingPage,
|
||||
EditProfilePage,
|
||||
InboxPage,
|
||||
LandingPage,
|
||||
|
|
@ -62,6 +63,18 @@ const routesConfiguration = [
|
|||
name: 'ListingPage',
|
||||
component: props => <ListingPage {...props} />,
|
||||
},
|
||||
{
|
||||
path: '/l/new',
|
||||
exact: true,
|
||||
name: 'NewListingPage',
|
||||
component: props => <EditListingPage {...props} type={'new'} />,
|
||||
},
|
||||
{
|
||||
path: '/l/:slug/:id/edit',
|
||||
exact: true,
|
||||
name: 'EditListingPage',
|
||||
component: props => <EditListingPage {...props} type={'edit'} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
"HeroSearchForm.search": "Search",
|
||||
"HeroSection.subTitle": "The largest online community to rent music studios",
|
||||
"HeroSection.title": "Book Studiotime anywhere",
|
||||
"EditListingForm.required": "Required",
|
||||
"EditListingForm.maxLength": "Must be {maxLength} characters or less",
|
||||
"ListingPage.loadingListingData": "Loading listing data",
|
||||
"ListingPage.noListingData": "Could not find listing data",
|
||||
"PageLayout.authInfoFailed": "Could not get authentication information.",
|
||||
|
|
|
|||
4
src/util/urlHelpers.jsx
Normal file
4
src/util/urlHelpers.jsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
export const createSlug = (str) =>
|
||||
encodeURIComponent(str.toLowerCase().split(' ').join('-'))
|
||||
14
src/util/validators.js
Normal file
14
src/util/validators.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Validator functions and helpers for Redux Forms
|
||||
*/
|
||||
|
||||
// Redux Form expects and undefined value for a successful validation
|
||||
const VALID = undefined;
|
||||
|
||||
export const required = message => value => {
|
||||
return value ? VALID : message;
|
||||
};
|
||||
|
||||
export const maxLength = (message, maximumLength) => value => {
|
||||
return !value || value.length <= maximumLength ? VALID : message;
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue