8 Commits
1.0.1 ... 1.4.0

Author SHA1 Message Date
77f2fb6bbe Remove gift list page 2017-08-27 12:24:35 +01:00
11c833d644 Add car park information to ceremony page 2017-07-16 17:35:08 +01:00
07f606b5dc Fix emails not sending due to RSVP end date refactor 2017-04-17 20:44:38 +01:00
2e32b09a5c Migrate RSVP end dates to per user 2017-04-16 17:29:41 +01:00
cdc6614c63 Create 2 RSVP end dates 2017-03-29 21:59:05 +01:00
75740699f1 Add link to gift list site 2017-03-14 16:12:14 +00:00
6d75606134 Create gift list page
Add generic vegetarian option to mains
Add additional content about Offley Place
2017-03-07 22:52:34 +00:00
a66bc1cedc Add less transparent bg to scrolled nav
Reposition the background images
2017-03-06 23:39:09 +00:00
24 changed files with 181 additions and 51 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "wedding", "name": "wedding",
"version": "1.0.1", "version": "1.4.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=6.5", "node": ">=6.5",

View File

@@ -13,6 +13,10 @@ html {
line-height: 1.375; /* ~22px */ line-height: 1.375; /* ~22px */
} }
body {
overflow-x: hidden;
}
a { a {
color: #0074c2; color: #0074c2;
} }

View File

@@ -4,8 +4,14 @@
right: 0; right: 0;
left: 0; left: 0;
height: 40px; height: 40px;
background: rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
z-index: 100;
background: rgba(0, 0, 0, 0.1);
transition: background 0.3s, ease-in;
}
.navDark {
background: rgba(60, 60, 60, 0.95);
} }
.container { .container {

View File

@@ -8,12 +8,40 @@ class Navigation extends React.Component {
current: PropTypes.string, current: PropTypes.string,
}; };
constructor(props) {
super(props);
this.state = {
scrollAtTop: true,
};
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll = (event) => {
const scrollTop = event.srcElement.body.scrollTop;
this.setState({
scrollAtTop: scrollTop < 10,
});
};
render() { render() {
if (!this.props.current) { if (!this.props.current) {
return null; return null;
} }
return ( return (
<div className={s.root} role="navigation"> <div
className={[
s.root,
this.state.scrollAtTop ? null : s.navDark,
].join(' ')}
role="navigation"
>
<div className={s.container}> <div className={s.container}>
<Link className={[s.link, this.props.current === 'home' ? s.active : null].join(' ')} to="/">RSVP</Link> <Link className={[s.link, this.props.current === 'home' ? s.active : null].join(' ')} to="/">RSVP</Link>
<Link className={[s.link, this.props.current === 'ceremony' ? s.active : null].join(' ')} to="/church">Church</Link> <Link className={[s.link, this.props.current === 'ceremony' ? s.active : null].join(' ')} to="/church">Church</Link>

View File

@@ -1,6 +1,6 @@
{ {
"name": "Navigation", "name": "Navigation",
"version": "1.0.0", "version": "1.1.1",
"private": true, "private": true,
"main": "./Navigation.js" "main": "./Navigation.js"
} }

View File

@@ -1,5 +1,5 @@
import Nodemailer from 'nodemailer'; import Nodemailer from 'nodemailer';
import { email, starters, mains, getRsvpEnd } from '../config'; import { email, starters, mains, prettifyDate } from '../config';
import Rollbar from './rollbar'; import Rollbar from './rollbar';
import Settings from '../data/models/Settings'; import Settings from '../data/models/Settings';
@@ -19,7 +19,7 @@ function getEmailText(person) {
Thanks for your RSVP, ${person.attending ? 'we\'re delighted to hear you will be attending' : 'we\'re sorry to hear you will not be attending'}.\n Thanks for your RSVP, ${person.attending ? 'we\'re delighted to hear you will be attending' : 'we\'re sorry to hear you will not be attending'}.\n
${person.attending ? `Your meal choices are ${starters[person.starter]} and ${mains[person.main]}.\n` : ''} ${person.attending ? `Your meal choices are ${starters[person.starter]} and ${mains[person.main]}.\n` : ''}
${person.attending && person.dietary && person.dietary.trim().length > 0 ? `Your dietary requirements are: ${person.dietary}.\n` : ''} ${person.attending && person.dietary && person.dietary.trim().length > 0 ? `Your dietary requirements are: ${person.dietary}.\n` : ''}
You can modify your RSVP up until ${getRsvpEnd()} here: https://danielandhana.com/${person.password}.\n You can modify your RSVP up until ${prettifyDate(person.enddate)} here: https://danielandhana.com/${person.password}.\n
${person.attending ? 'See you at our wedding,' : 'Hope to see you soon,'}\n ${person.attending ? 'See you at our wedding,' : 'Hope to see you soon,'}\n
Daniel and Hana Daniel and Hana
`; `;
@@ -30,7 +30,7 @@ function getEmailHTML(person) {
<p>Thanks for your RSVP, ${person.attending ? 'we\'re delighted to hear you will be attending' : 'we\'re sorry to hear you will not be attending'}.</p> <p>Thanks for your RSVP, ${person.attending ? 'we\'re delighted to hear you will be attending' : 'we\'re sorry to hear you will not be attending'}.</p>
${person.attending ? `<p>Your meal choices are <b>${starters[person.starter]}</b> and <b>${mains[person.main]}</b>.</p>` : ''} ${person.attending ? `<p>Your meal choices are <b>${starters[person.starter]}</b> and <b>${mains[person.main]}</b>.</p>` : ''}
${person.attending && person.dietary && person.dietary.trim().length > 0 ? `<p>Your dietary requirements are: <b>${person.dietary}</b>.</p>` : ''} ${person.attending && person.dietary && person.dietary.trim().length > 0 ? `<p>Your dietary requirements are: <b>${person.dietary}</b>.</p>` : ''}
<p>You can modify your RSVP up until <b>${getRsvpEnd()}</b> <a href="https://danielandhana.com/${person.password}">here</a>.</p> <p>You can modify your RSVP up until <b>${prettifyDate(person.enddate)}</b> <a href="https://danielandhana.com/${person.password}">here</a>.</p>
<p>${person.attending ? 'See you at our wedding,' : 'Hope to see you soon,'}</p> <p>${person.attending ? 'See you at our wedding,' : 'Hope to see you soon,'}</p>
<p>Daniel and Hana</p> <p>Daniel and Hana</p>
`; `;

View File

@@ -66,6 +66,10 @@ const Person = Model.define('Person', {
defaultValue: -1, defaultValue: -1,
}, },
enddate: {
type: DataType.DATE,
},
}, { }, {
indexes: [ indexes: [

View File

@@ -20,6 +20,7 @@ const PersonItemType = new ObjectType({
dietary: { type: StringType }, dietary: { type: StringType },
starter: { type: StringType }, starter: { type: StringType },
main: { type: StringType }, main: { type: StringType },
enddate: { type: StringType },
}, },
}); });

View File

@@ -12,6 +12,7 @@
background: url('/img/offley_2560.jpg'); background: url('/img/offley_2560.jpg');
background-size: cover; background-size: cover;
background-position-x: 54%; background-position-x: 54%;
background-position-y: 66%;
background-color: #c1c1c1; background-color: #c1c1c1;
filter: grayscale(1) brightness(0.4); filter: grayscale(1) brightness(0.4);
z-index: -1; z-index: -1;

View File

@@ -3,6 +3,7 @@ import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './AdminPeople.css'; import s from './AdminPeople.css';
import Link from '../../components/Link'; import Link from '../../components/Link';
import Button from '../../components/Button'; import Button from '../../components/Button';
import { prettifyDate } from '../../config';
class AdminPeople extends React.Component { class AdminPeople extends React.Component {
static propTypes = { static propTypes = {
@@ -16,6 +17,7 @@ class AdminPeople extends React.Component {
email: PropTypes.string, email: PropTypes.string,
password: PropTypes.string, password: PropTypes.string,
attending: PropTypes.boolean, attending: PropTypes.boolean,
enddate: PropTypes.string,
})).isRequired, })).isRequired,
}; };
@@ -138,6 +140,22 @@ class AdminPeople extends React.Component {
>⬆</span> >⬆</span>
<a href="#sortcolumn" onClick={event => this.sortPeople(event, 'ceremony')}>Ceremony</a> <a href="#sortcolumn" onClick={event => this.sortPeople(event, 'ceremony')}>Ceremony</a>
</td> </td>
<td>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'enddate' && this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬇</span>
<span
className={[
s.sortIcon,
this.state.sortedBy === 'enddate' && !this.state.sortDirection ? s.showSortIcon : null,
].join(' ')}
>⬆</span>
<a href="#sortcolumn" onClick={event => this.sortPeople(event, 'enddate')}>End date</a>
</td>
<td> <td>
<span <span
className={[ className={[
@@ -163,6 +181,7 @@ class AdminPeople extends React.Component {
<td>{person.email}</td> <td>{person.email}</td>
<td><Link to={`/rsvp/${person.password}`}>{person.password}</Link></td> <td><Link to={`/rsvp/${person.password}`}>{person.password}</Link></td>
<td>{person.ceremony ? 'Full day' : 'Evening only'}</td> <td>{person.ceremony ? 'Full day' : 'Evening only'}</td>
<td>{prettifyDate(person.enddate)}</td>
<td>{person.completed ? 'Yes' : 'No'}</td> <td>{person.completed ? 'Yes' : 'No'}</td>
<td> <td>
<Button to={`/admin/person/${person.key}`}>Edit</Button> <Button to={`/admin/person/${person.key}`}>Edit</Button>
@@ -239,6 +258,17 @@ class AdminPeople extends React.Component {
<option value="false">Evening only</option> <option value="false">Evening only</option>
</select> </select>
</div> </div>
<div className={s.formGroup}>
<label className={s.label} htmlFor="enddate">
End date:
</label>
<input
className={s.input}
type="date"
id="enddate"
name="enddate"
/>
</div>
<div className={s.formGroup}> <div className={s.formGroup}>
<label className={s.label} htmlFor="password"> <label className={s.label} htmlFor="password">
Password (blank will autogenerate): Password (blank will autogenerate):

View File

@@ -9,7 +9,7 @@ export default {
path: '/admin/people', path: '/admin/people',
async action() { async action() {
const resp = await fetchQL('{getAllPersons{key,firstname,lastname,email,ceremony,completed,password}}'); const resp = await fetchQL('{getAllPersons{key,firstname,lastname,email,ceremony,completed,password,enddate}}');
let people = await resp.json(); let people = await resp.json();
if (people.data && people.data.getAllPersons) { if (people.data && people.data.getAllPersons) {

View File

@@ -15,6 +15,7 @@ class AdminPerson extends React.Component {
password: PropTypes.string, password: PropTypes.string,
attending: PropTypes.boolean, attending: PropTypes.boolean,
ceremony: PropTypes.boolean, ceremony: PropTypes.boolean,
enddate: PropTypes.string,
}).isRequired, }).isRequired,
}; };
@@ -23,6 +24,14 @@ class AdminPerson extends React.Component {
lastname: this.props.person.lastname, lastname: this.props.person.lastname,
}; };
getISODate = (date) => {
const d = new Date(date);
const year = d.getFullYear();
const month = d.getMonth() + 1 < 10 ? `0${d.getMonth() + 1}` : d.getMonth() + 1;
const day = d.getDate() < 10 ? `0${d.getDate()}` : d.getDate();
return `${year}-${month}-${day}`;
}
handleFirstnameChange = (event) => { handleFirstnameChange = (event) => {
this.setState({ firstname: event.target.value }); this.setState({ firstname: event.target.value });
} }
@@ -94,6 +103,18 @@ class AdminPerson extends React.Component {
<option value="false">Evening only</option> <option value="false">Evening only</option>
</select> </select>
</div> </div>
<div className={s.formGroup}>
<label className={s.label} htmlFor="enddate">
End date:
</label>
<input
className={s.input}
type="date"
id="enddate"
name="enddate"
defaultValue={this.getISODate(this.props.person.enddate)}
/>
</div>
<div className={s.formGroup}> <div className={s.formGroup}>
<label className={s.label} htmlFor="password"> <label className={s.label} htmlFor="password">
Password: Password:

View File

@@ -11,7 +11,7 @@ export default {
return { redirect: '/admin' }; return { redirect: '/admin' };
} }
const resp = await fetchQL('{getPersonByKey{key,firstname,lastname,email,completed,password,ceremony,starter,main,dietary}}', { const resp = await fetchQL('{getPersonByKey{key,firstname,lastname,email,completed,password,ceremony,starter,main,dietary,enddate}}', {
key: params.key, key: params.key,
}); });

View File

@@ -12,6 +12,7 @@
background: url('/img/church_2560.jpg'); background: url('/img/church_2560.jpg');
background-size: cover; background-size: cover;
background-position-x: 54%; background-position-x: 54%;
background-position-y: 66%;
background-color: #c1c1c1; background-color: #c1c1c1;
filter: grayscale(1) brightness(0.4); filter: grayscale(1) brightness(0.4);
z-index: -1; z-index: -1;

View File

@@ -12,7 +12,21 @@ const church = {
showInfo: true, showInfo: true,
}; };
const carPark = {
title: 'Langley Hill Car Park',
position: {
lat: 51.7121,
lng: -0.4507,
},
showInfo: true,
};
class Ceremony extends React.Component { class Ceremony extends React.Component {
constructor(props) {
super(props);
this.markers = [church, carPark];
}
render() { render() {
return ( return (
<div className={s.root}> <div className={s.root}>
@@ -29,11 +43,11 @@ class Ceremony extends React.Component {
<p>The ceremony begins at 1pm</p> <p>The ceremony begins at 1pm</p>
<h3>Where</h3> <h3>Where</h3>
<p>The church is located at the southern end of Kings Langley high street. <p>The church is located at the southern end of Kings Langley high street.
Parking is available across the road at the Rose and Crown pub.</p> Free parking is available on Langley Hill.</p>
<Map <Map
defaultZoom={14} defaultZoom={15}
defaultCenter={church.position} defaultCenter={church.position}
markers={[church]} markers={this.markers}
/> />
<h3>About the church</h3> <h3>About the church</h3>
<p>All Saints Church is a medieval church with parts dating from the 13th century. <p>All Saints Church is a medieval church with parts dating from the 13th century.

View File

@@ -12,6 +12,7 @@
background: url('/img/prague_2560.jpg'); background: url('/img/prague_2560.jpg');
background-size: cover; background-size: cover;
background-position-x: 54%; background-position-x: 54%;
background-position-y: 66%;
background-color: #c1c1c1; background-color: #c1c1c1;
filter: grayscale(1) brightness(0.6); filter: grayscale(1) brightness(0.6);
z-index: -1; z-index: -1;

View File

@@ -12,6 +12,7 @@
background: url('/img/offley_2560.jpg'); background: url('/img/offley_2560.jpg');
background-size: cover; background-size: cover;
background-position-x: 54%; background-position-x: 54%;
background-position-y: 66%;
background-color: #c1c1c1; background-color: #c1c1c1;
filter: grayscale(1) brightness(0.4); filter: grayscale(1) brightness(0.4);
z-index: -1; z-index: -1;

View File

@@ -39,6 +39,23 @@ class Reception extends React.Component {
defaultCenter={offley.position} defaultCenter={offley.position}
markers={[offley]} markers={[offley]}
/> />
<h3>About Offley Place</h3>
<p>Legend has it that in the 8th century, King Offa discovered the bones of
Saint Alban and set about creating St Albans Abbey in a tribute to him.
While it was being completed he looked for a place to call home and
discovered the local area. After building a grand palace the village is
thought to have become known as Great Off Lee.</p>
<p>Great Offley Place was first built in the 17th century. Rebuilt in 1810
but still retaining a Tudor porch and a 17th century wing, the grand house
has a long history which has seen the same family line keep ownership for
an impressive 350 years.</p>
<p>In 1929 the family ownership was brought to an end when the last owner,
Guy George, sold the estate to a director of WH Smith who was well liked
in the village and built a cricket pavilion and pitch.</p>
<p>When World War II broke out it was sold again, this time to become a
teacher training college, and then finally in 2003 it was taken over again
and transformed into the luxury hotel, wedding venue and restaurant it
is today.</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -12,6 +12,7 @@
background: url('/img/cervenalhota_2560.jpg'); background: url('/img/cervenalhota_2560.jpg');
background-size: cover; background-size: cover;
background-position-x: 30%; background-position-x: 30%;
background-position-y: 66%;
background-color: #c1c1c1; background-color: #c1c1c1;
filter: grayscale(1) brightness(0.6); filter: grayscale(1) brightness(0.6);
z-index: -1; z-index: -1;

View File

@@ -14,6 +14,7 @@ class Rsvp extends React.Component {
dietary: PropTypes.string, dietary: PropTypes.string,
starter: PropTypes.string, starter: PropTypes.string,
main: PropTypes.string, main: PropTypes.string,
enddate: PropTypes.string,
})).isRequired, })).isRequired,
rsvpEnd: PropTypes.string.isRequired, rsvpEnd: PropTypes.string.isRequired,
isEditable: PropTypes.bool.isRequired, isEditable: PropTypes.bool.isRequired,

View File

@@ -3,16 +3,15 @@ import Layout from '../../components/Layout';
import fetchQL from '../../core/fetchQL'; import fetchQL from '../../core/fetchQL';
import Rsvp from './Rsvp'; import Rsvp from './Rsvp';
import NotFound from '../notFound/NotFound'; import NotFound from '../notFound/NotFound';
import { rsvpEndDate, getRsvpEnd } from '../../config'; import { prettifyDate } from '../../config';
let title = 'RSVP'; let title = 'RSVP';
export default { export default {
path: '/rsvp/:password', path: '/rsvp/:password',
isEditable() { isEditable(date) {
return new Date() < rsvpEndDate; return new Date() < new Date(date);
}, },
async action({ params }) { async action({ params }) {
@@ -20,7 +19,7 @@ export default {
return { redirect: '/' }; return { redirect: '/' };
} }
const resp = await fetchQL('{getPersonsByPassword{key,firstname,lastname,attending,ceremony,dietary,starter,main}}', { const resp = await fetchQL('{getPersonsByPassword{key,firstname,lastname,attending,ceremony,dietary,starter,main,enddate}}', {
password: params.password, password: params.password,
}); });
@@ -35,6 +34,9 @@ export default {
}; };
} }
// Assuming all connected guests have the same invite
const firstDate = people[0].enddate;
return { return {
title, title,
component: ( component: (
@@ -42,8 +44,8 @@ export default {
<Rsvp <Rsvp
title={title} title={title}
people={people} people={people}
rsvpEnd={getRsvpEnd()} rsvpEnd={prettifyDate(firstDate)}
isEditable={this.isEditable()} isEditable={this.isEditable(firstDate)}
/> />
</Layout> </Layout>
), ),

View File

@@ -12,6 +12,7 @@
background: url('/img/offley_2560.jpg'); background: url('/img/offley_2560.jpg');
background-size: cover; background-size: cover;
background-position-x: 54%; background-position-x: 54%;
background-position-y: 66%;
background-color: #c1c1c1; background-color: #c1c1c1;
filter: grayscale(1) brightness(0.4); filter: grayscale(1) brightness(0.4);
z-index: -1; z-index: -1;

View File

@@ -21,7 +21,7 @@ import models from './data/models';
import schema from './data/schema'; import schema from './data/schema';
import routes from './routes'; import routes from './routes';
import assets from './assets'; // eslint-disable-line import/no-unresolved import assets from './assets'; // eslint-disable-line import/no-unresolved
import { port, auth, starters, mains, rsvpEndDate, exportApiKey } from './config'; import { port, auth, starters, mains, exportApiKey } from './config';
import Person from './data/models/Person'; import Person from './data/models/Person';
import Settings from './data/models/Settings'; import Settings from './data/models/Settings';
import Rollbar from './core/rollbar'; import Rollbar from './core/rollbar';
@@ -149,32 +149,27 @@ app.get('/rsvp/:code', bouncer.block, (req, res, next) => {
app.post('/rsvp/save', (req, res) => { app.post('/rsvp/save', (req, res) => {
try { try {
if (new Date() > rsvpEndDate) { Person.findOne({
res.json({ where: { key: req.body.key },
success: false, }).then(data => {
err: 'RSVP end date has elapsed', const endDate = new Date(data.enddate);
}); if (new Date() < endDate) {
} else { const update = {};
const update = {}; update[req.body.prop] = req.body.value;
update[req.body.prop] = req.body.value; return Person.update(update, {
Person.update(update, { where: {
where: { key: req.body.key,
key: req.body.key, },
}, }).then(() => {
}).then(() => {
Person.findOne({
where: { key: req.body.key },
}).then(data => {
if (data.completed) { if (data.completed) {
const attending = `\nAttending: ${data.attending ? 'Yes' : 'No'}`; const attending = `\nAttending: ${data.attending ? 'Yes' : 'No'}`;
const starter = (data.attending && data.starter !== -1) ? `\nStarter: ${starters[data.starter]}` : ''; const starter = (data.attending && data.starter !== -1) ? `\nStarter: ${starters[data.starter]}` : '';
const main = (data.attending && data.main !== -1) ? `\nMain: ${mains[data.main]}` : ''; const main = (data.attending && data.main !== -1) ? `\nMain: ${mains[data.main]}` : '';
const dietary = (data.attending && data.dietary) ? `\nDietary requirements: ${data.dietary}` : ''; const dietary = (data.attending && data.dietary) ? `\nDietary requirements: ${data.dietary}` : '';
sendSlackMsgWithDebounce(`${data.firstname} ${data.lastname} has saved their RSVP:${attending}${starter}${main}${dietary}`, '#wedding-rsvps', req.body.key); sendSlackMsgWithDebounce(`${data.firstname} ${data.lastname} has saved their RSVP:${attending}${starter}${main}${dietary}`, '#wedding-rsvps', req.body.key);
sendRSVPEmailWithDebounce(data); sendRSVPEmailWithDebounce(data);
} }
res.json({ return res.json({
success: true, success: true,
prop: req.body.prop, prop: req.body.prop,
}); });
@@ -185,14 +180,18 @@ app.post('/rsvp/save', (req, res) => {
err, err,
}); });
}); });
}).catch(err => { }
Rollbar.handleError(err); return res.json({
res.json({ success: false,
success: false, err: 'RSVP end date has elapsed',
err,
});
}); });
} }).catch(err => {
Rollbar.handleError(err);
res.json({
success: false,
err,
});
});
} catch (err) { } catch (err) {
Rollbar.handleError(err); Rollbar.handleError(err);
res.json({ res.json({
@@ -243,6 +242,7 @@ app.post('/admin/person', (req, res, next) => {
lastname: req.body.lastname, lastname: req.body.lastname,
email: req.body.email, email: req.body.email,
ceremony: req.body.ceremony, ceremony: req.body.ceremony,
enddate: new Date(req.body.enddate),
}; };
if (req.body.password && req.body.password !== '') { if (req.body.password && req.body.password !== '') {

View File

@@ -4042,7 +4042,7 @@ lodash.toplainobject@^3.0.0:
lodash._basecopy "^3.0.0" lodash._basecopy "^3.0.0"
lodash.keysin "^3.0.0" lodash.keysin "^3.0.0"
lodash@4.12.0, lodash@^4.2.0: lodash@4.12.0:
version "4.12.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.12.0.tgz#2bd6dc46a040f59e686c972ed21d93dc59053258" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.12.0.tgz#2bd6dc46a040f59e686c972ed21d93dc59053258"
@@ -4054,11 +4054,7 @@ lodash@^3.10.1:
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
lodash@^4.0.0, lodash@^4.15.0, lodash@^4.16.4, lodash@^4.17.2, lodash@^4.6.1: lodash@^4.0.0, lodash@^4.1.0, lodash@^4.15.0, lodash@^4.16.2, lodash@^4.16.4, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.6.1:
version "4.17.2"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42"
lodash@^4.1.0, lodash@^4.16.2, lodash@^4.17.4, lodash@^4.3.0:
version "4.17.4" version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"