Compare commits

...

202 commits

Author SHA1 Message Date
Omar Najjar
7fa386fb0a
Merge pull request #1 from najomar68/omar-stashDesignChanges
changed logo and background images
2019-09-30 13:37:51 +10:00
Omar
1c77fadc77 stash changes 2019-09-30 13:03:11 +10:00
Juho Makkonen
3fb0c12770
Add link to Slack 2019-09-17 16:56:54 +03:00
Vesa Luusua
1968d26de8
Merge pull request #1202 from sharetribe/v3.5.1
New release: v3.5.1
2019-09-17 12:55:41 +03:00
Vesa Luusua
57c243f191 New release: v3.5.1 2019-09-17 12:53:21 +03:00
Vesa Luusua
e2213a8b18
Merge pull request #1200 from rush1506/master
[fix bugs] current CustomOverlayView in SearchMapWithGoogleMap does not work with new React version
2019-09-17 12:46:02 +03:00
tam.vu
dc6b6f813f Run prettier format for modified files 2019-09-17 13:33:25 +07:00
tam.vu
3ee4612504 Change CustomOverlayView in SearchMapWithGoogleMap:
- Adding onAdd and onRemove function to improve performance
- Rewrite current draw() method to compatible with newer react version
2019-09-16 14:00:44 +07:00
Jenni Laakso
483c590aef
Merge pull request #1197 from sharetribe/v3.5.0
v3.5.0
2019-09-13 13:10:40 +03:00
Jenni Nurmi
64351bb3a2 Update changelog 2019-09-13 13:08:27 +03:00
Jenni Laakso
7ac10dc42e
Merge pull request #1195 from sharetribe/time-to-bookingbreakdown
Show also time in booking breakdown if needed
2019-09-13 12:11:16 +03:00
Jenni Nurmi
54b97cb15e Update changelog 2019-09-13 12:07:31 +03:00
Jenni Nurmi
6e40507ea8 Use updated BookingBreakdown component 2019-09-13 12:07:31 +03:00
Jenni Nurmi
770a487c4d Fix proptype in BookingTimeInfo 2019-09-13 12:07:31 +03:00
Jenni Nurmi
dbf963445c Update BookingBreakdown 2019-09-13 12:07:22 +03:00
Jenni Nurmi
a00bbbfd80 Add new font size 15px 2019-09-12 11:10:01 +03:00
Jenni Nurmi
f3608721ae Create LineItemBasePriceMaybe component 2019-09-12 11:08:36 +03:00
Jenni Laakso
a707c4b4dd
Merge pull request #1194 from sharetribe/show-time-on-inboxpage
Show booking  date and time correctly on InboxPage
2019-09-10 17:25:23 +03:00
Jenni Nurmi
697003d1c6 Update changelog 2019-09-10 17:22:58 +03:00
Jenni Nurmi
570378f88c Use BookingTimeInfo on InboxPage 2019-09-10 17:22:58 +03:00
Jenni Nurmi
4c97c19858 Create BookingTimeInfo component 2019-09-10 17:22:53 +03:00
Jenni Laakso
cca0539c74
Merge pull request #1193 from sharetribe/update-es-translations
Update Spanish translations
2019-09-09 16:58:33 +03:00
Jenni Nurmi
d9a36bacde Update changelog 2019-09-09 10:08:11 +03:00
Jenni Nurmi
281050e696 Update Spanish translations 2019-09-09 10:07:12 +03:00
Vesa Luusua
3abe866e01
Merge pull request #1190 from sharetribe/missing-yarn-lock
Missing yarn.lock change
2019-08-30 11:45:14 +03:00
Vesa Luusua
7ba8a863b1 Update changelog 2019-08-30 11:43:09 +03:00
Vesa Luusua
579eb478ed For some reason yarn.lock was not up to date 2019-08-30 11:41:15 +03:00
Vesa Luusua
c98738aca9
Merge pull request #1189 from sharetribe/v3.4.0
New release: v3.4.0
2019-08-29 18:05:23 +03:00
Vesa Luusua
fbe7f9af37 New release: v3.4.0 2019-08-29 17:40:41 +03:00
Jenni Laakso
2390beab02
Merge pull request #1181 from sharetribe/update-intl
Update intl
2019-08-29 17:34:16 +03:00
Jenni Nurmi
fe92265201 Update changelog 2019-08-29 16:55:07 +03:00
Jenni Nurmi
efd7ac1a68 Update tests 2019-08-29 16:37:47 +03:00
Jenni Nurmi
3a88c0f386 Add textComponent prop to IntlProvider 2019-08-29 16:37:47 +03:00
Jenni Nurmi
a173e7271e Don't use addLocaleData 2019-08-29 16:37:47 +03:00
Jenni Nurmi
601706cc5f Use react-intl through util file 2019-08-29 16:37:47 +03:00
Jenni Nurmi
40c4bfb8fe Update react-intl to 3.1.13 2019-08-29 16:37:47 +03:00
Vesa Luusua
d6d456856d
Merge pull request #1186 from sharetribe/update-helmet-again
Update helmet again
2019-08-29 16:27:32 +03:00
Vesa Luusua
e08e23a8b1 Update changelog 2019-08-29 16:25:26 +03:00
Vesa Luusua
b156397ea0 Update helmet (v3.20.0 > v3.20.1) 2019-08-29 16:24:52 +03:00
Vesa Luusua
4d1e244629
Merge pull request #1188 from sharetribe/circumvent-lodash-vulnerability
Circumvent lodash vulnerability
2019-08-29 16:24:26 +03:00
Vesa Luusua
d85bb55f4f Update changelog 2019-08-29 16:21:31 +03:00
Vesa Luusua
ef11c17384 Fix lodash vulnerability: force newer version for react-google-maps and react-dates 2019-08-29 16:20:11 +03:00
Jenni Laakso
b5bde9da43
Merge pull request #1187 from sharetribe/add-changelog
Add React update to changelog
2019-08-29 16:18:51 +03:00
Jenni Nurmi
76f842f8df Add React update to changelog 2019-08-29 16:14:22 +03:00
Jenni Laakso
959ec06b2c
Merge pull request #1172 from sharetribe/update-react
Update react to 16.9.0
2019-08-29 16:00:19 +03:00
Jenni Nurmi
d5be9eb045 Temporarily remove tests related to react-dates to prevent deprecation warings 2019-08-29 15:44:51 +03:00
Vesa Luusua
e4222beac3 Update yarn.lock after merge conflict 2019-08-29 15:15:58 +03:00
Jenni Nurmi
c4bae634ec Don't use componentWillReceiveProps in Routes 2019-08-29 15:15:58 +03:00
Jenni Nurmi
5f7ffd737f Don't use componentWillReceiveProps in FieldDateRangeInput 2019-08-29 15:15:58 +03:00
Jenni Nurmi
3720591dda Don't use componentWillReceiveProps 2019-08-29 15:15:57 +03:00
Jenni Nurmi
34c4903bad Don't use componentWillReceiveProps related to SearchMap component 2019-08-29 15:15:57 +03:00
Jenni Nurmi
c13413170f Don't use componentWillReceiveProps related to modals 2019-08-29 15:15:57 +03:00
Jenni Nurmi
04fbf454aa Don't use deprecated lifecycle methods in FieldBirthdayInput 2019-08-29 15:15:57 +03:00
Jenni Nurmi
41972f70b3 Don't use componentWillMount 2019-08-29 15:15:57 +03:00
Jenni Nurmi
816584a93e Update react-dom to v16.9.0 2019-08-29 15:15:57 +03:00
Jenni Nurmi
4d906e2e96 Update react-test-renderer to 16.9.0 2019-08-29 15:15:57 +03:00
Jenni Nurmi
60232968da Update react to 16.9.0 2019-08-29 15:15:57 +03:00
Vesa Luusua
b8449fc588
Merge pull request #1185 from sharetribe/fix-willunmount-profilesettingsform
Fix componentWillUnmount on ProfileSettingsForm
2019-08-29 13:26:13 +03:00
Vesa Luusua
bace4600fd Update changelog 2019-08-29 13:19:57 +03:00
Vesa Luusua
fb384544df Fix willUnmount bug from ProfileSettingsForm 2019-08-29 13:17:51 +03:00
Vesa Luusua
8ed76db221
Merge pull request #1183 from sharetribe/fix-availabilityplan-prop
Fix availabilityplan prop validation in EditListingAvailabilityForm
2019-08-29 12:59:26 +03:00
Vesa Luusua
64746f29d7 Update changelog 2019-08-29 12:57:21 +03:00
Vesa Luusua
eed0b9b0e8 Fix: availabilityPlan prop in EditListingAvailabilityForm was missing. 2019-08-29 12:56:49 +03:00
Vesa Luusua
0ac72e2ff4
Merge pull request #1182 from sharetribe/fix-fieldbirthdayinput
Fix FieldBirthdayInput: valueFromForm prop didn't work anymore
2019-08-29 12:55:50 +03:00
Vesa Luusua
a3fd957971 Update changelog 2019-08-29 12:53:05 +03:00
Vesa Luusua
0509f5224f Fix FieldBirthdayInput: valueFromForm prop didn't work anymore 2019-08-29 12:52:24 +03:00
Jenni Laakso
3b463b64be
Merge pull request #1184 from sharetribe/translation-updates
Update French and German translations
2019-08-29 12:46:15 +03:00
Jenni Nurmi
faf3a4337a Update changelog 2019-08-29 10:08:45 +03:00
Jenni Nurmi
3d3af94a24 Update French translations 2019-08-29 10:04:37 +03:00
Jenni Nurmi
eaeece5f5d Update German translations 2019-08-29 10:04:23 +03:00
Vesa Luusua
1b953c4c78
Merge pull request #1180 from sharetribe/update-changelog-after-react-helmet-async
Update changelog
2019-08-28 15:13:18 +03:00
Vesa Luusua
28fcdc3496 Update changelog 2019-08-28 15:11:14 +03:00
Vesa Luusua
c2eb3c5cb0
Merge pull request #1179 from sharetribe/migrate-to-react-helmet-async
Migrate from react-helmet to react-helmet-async
2019-08-28 15:08:27 +03:00
Vesa Luusua
64c8df222d Migrate from react-helmet to react-helmet-async 2019-08-28 14:58:14 +03:00
Jenni Laakso
67a15563f3
Merge pull request #1177 from sharetribe/sanitize-css-to-own-file
Move sanitize.css to assets
2019-08-28 14:11:42 +03:00
Jenni Nurmi
0e98cc2294 Update changelog 2019-08-28 14:09:16 +03:00
Jenni Nurmi
b05b138550 Move sanitize.css to assets 2019-08-28 14:00:51 +03:00
Vesa Luusua
5955732b22
Merge pull request #1178 from sharetribe/fix-tests-after-react-redux
Fix: app.test.js started to throw warnings due to react-redux update
2019-08-28 13:44:23 +03:00
Vesa Luusua
30b4b4fbdd Update changelog 2019-08-28 13:25:41 +03:00
Vesa Luusua
709c36e623 Fix: app.test.js started to throw warnings due to react-redux update 2019-08-28 13:17:00 +03:00
Vesa Luusua
833d9fd76c
Merge pull request #1176 from sharetribe/update-react-redux
Update react-redux v5.1.1 -> v7.1.1
2019-08-27 16:40:20 +03:00
Jenni Laakso
fab719b977
Merge pull request #1175 from sharetribe/update-seedrandom
Update seedrandom to 3.0.3
2019-08-27 16:29:16 +03:00
Vesa Luusua
4232a23fa5 Update react-redux v5.1.1 -> v7.1.1 2019-08-27 16:26:38 +03:00
Jenni Nurmi
22b0b7c47f Update changelog 2019-08-27 16:17:06 +03:00
Jenni Nurmi
ebe069b162 Update seedrandom to 3.0.3 2019-08-27 16:13:14 +03:00
Jenni Laakso
a82dd4039b
Merge pull request #1174 from sharetribe/update-inquirer
Update inquirer to 7.0.0
2019-08-27 15:48:34 +03:00
Jenni Nurmi
edfe3e7095 Update changelog 2019-08-27 15:41:36 +03:00
Jenni Nurmi
c3a99e78e4 Update inquirer to 7.0.0 2019-08-27 15:40:43 +03:00
Vesa Luusua
dbfb5141a4
Merge pull request #1173 from sharetribe/update-final-form
Update Final Form et al.
2019-08-27 15:21:16 +03:00
Vesa Luusua
2fc75aec76 Update changelog 2019-08-27 15:14:55 +03:00
Vesa Luusua
87256f1fa7 Fix: do not remove previous state when publishing a listing 2019-08-27 15:05:26 +03:00
Vesa Luusua
3c80e194a8 Added missing proptype check 2019-08-27 14:16:31 +03:00
Vesa Luusua
cbd6b738a0 fix FieldBirthdayInput: example was broken 2019-08-27 14:15:34 +03:00
Vesa Luusua
dc61925e77 fix: fields having duplicate or undefined ids 2019-08-27 14:14:33 +03:00
Vesa Luusua
9eb4e7a125 pre-final-form naming pattern: form prop was conflicting with form api 2019-08-27 14:12:54 +03:00
Vesa Luusua
494ad56147 final form update: type is nowadays inside input props 2019-08-27 13:52:43 +03:00
Vesa Luusua
35039f5643 final form update: old recommendation format with null is not accepted anymore 2019-08-27 13:50:19 +03:00
Vesa Luusua
875b7ac108 Update final-form, final-form-arrays, react-final-form, react-final-form-arrays 2019-08-27 13:44:45 +03:00
Jenni Laakso
5fc4f43217
Merge pull request #1171 from sharetribe/update-react-dates
Update react-dates to 20.3.0
2019-08-26 17:06:53 +03:00
Jenni Nurmi
d50be5cdf8 Update changelog 2019-08-26 17:03:52 +03:00
Jenni Nurmi
0bb009d897 Update react-dates 2019-08-26 17:03:29 +03:00
Jenni Laakso
76c61bc52d
Merge pull request #1170 from sharetribe/update-prettier
Update prettier to 1.18.2
2019-08-26 17:03:02 +03:00
Jenni Nurmi
3f5c983e44 Update changelog 2019-08-26 17:00:27 +03:00
Jenni Nurmi
72d98a5912 Update prettier 2019-08-26 16:59:39 +03:00
Jenni Laakso
6186480a94
Merge pull request #1169 from sharetribe/update-path-to-regexp
Update path-to-regexp to 3.0.0
2019-08-26 16:58:52 +03:00
Jenni Nurmi
e138a05194 Update changelog 2019-08-26 10:29:15 +03:00
Jenni Nurmi
593777ffa1 Update path-to-regexp to 3.0.0 2019-08-26 10:27:07 +03:00
Jenni Laakso
a83a4057db
Merge pull request #1167 from sharetribe/update-sharetribe-script
Update sharetribe-scripts to 3.1.1
2019-08-23 16:49:02 +03:00
Jenni Nurmi
3cda4f2c81 Update sharetribe-scripts to 3.1.1-alpha 2019-08-23 16:43:59 +03:00
Jenni Laakso
033060c1a5
Merge pull request #1166 from sharetribe/update-en-translations
Add card number to remove card modal
2019-08-23 13:43:21 +03:00
Jenni Nurmi
4a1ab6cc02 Update changelog 2019-08-23 13:41:27 +03:00
Jenni Nurmi
ede229944f Add card number to remove card modal 2019-08-23 13:41:21 +03:00
Jenni Laakso
17cf7e5ee5
Merge pull request #1164 from sharetribe/update-deps
Update Sentry
2019-08-23 13:31:09 +03:00
Jenni Nurmi
9724ed565b Update changelog 2019-08-23 12:05:56 +03:00
Jenni Nurmi
55b043bb7b Update Sentry 2019-08-23 12:05:11 +03:00
Vesa Luusua
c3b8d68c74
Merge pull request #1165 from sharetribe/update-changelog
Update changelog
2019-08-23 12:04:50 +03:00
Vesa Luusua
d6a90c3271 Update changelog 2019-08-23 12:02:06 +03:00
Vesa Luusua
c90c83a10e
Merge pull request #1163 from sharetribe/update-dependencies-minor
Update dependencies: most of the batch and minor updates
2019-08-23 11:49:25 +03:00
Vesa Luusua
bc3f50d702 Update dependencies: most of the batch and minor updates 2019-08-23 11:44:25 +03:00
Jenni Laakso
239914cab0
Merge pull request #1138 from sharetribe/save-credit-card
Save payment card
2019-08-22 16:45:49 +03:00
Jenni Nurmi
ac1559b7dd Update changelog 2019-08-22 16:41:31 +03:00
Jenni Nurmi
95e5f36a48 SavedCardDetails: use portal for modal 2019-08-22 15:44:19 +03:00
Vesa Luusua
120a4b556d TransactionPanel: savePaymentMethodFailed flag used 2019-08-22 15:44:14 +03:00
Vesa Luusua
519ec667da TransactionPage: savePaymentMethodFailed flag added 2019-08-22 15:44:14 +03:00
Vesa Luusua
a62674f187 CheckoutPage: saves card for later use 2019-08-22 15:44:14 +03:00
Vesa Luusua
47fb3b2951 CheckoutPage.duck: fetch stripeCustomer relantionship for currentUser 2019-08-22 15:44:14 +03:00
Vesa Luusua
a6dac66f8e FieldCheckbox: add useSuccessColor prop 2019-08-22 15:44:14 +03:00
Vesa Luusua
dc34e4463d Add new type: defaultPaymentMethod 2019-08-22 15:44:14 +03:00
Jenni Nurmi
72b4f342d9 Create LayoutWrapperAccountSettingsSideNav 2019-08-22 15:44:14 +03:00
Jenni Nurmi
905f99043b Create PaymentMethodsPage 2019-08-22 15:44:14 +03:00
Jenni Nurmi
d4c265c014 Create PaymentMethodsForm 2019-08-22 15:44:14 +03:00
Jenni Nurmi
8ffa38bcc0 Move StripePaymentAddress to components 2019-08-22 15:44:09 +03:00
Jenni Nurmi
40a3ab09aa Create SavedCardDetailsComponent 2019-08-22 15:06:11 +03:00
Jenni Nurmi
89e7846519 IconCheckmark: big and small versions 2019-08-22 15:06:11 +03:00
Janne Koivistoinen
c1bfed5e7d Add more directions to IconArrowHead 2019-08-22 15:06:11 +03:00
Jenni Nurmi
262bfba6b3 Create IconCard 2019-08-22 15:06:11 +03:00
Vesa Luusua
74c0ce96e5 Add ensureStripeCustomer and ensurePaymentMethodCard 2019-08-22 15:06:11 +03:00
Jenni Nurmi
6521ec7ab7 Add handleCardSetup to stripe.duck.js 2019-08-22 15:06:11 +03:00
Vesa Luusua
839afb6f4b user.duck.js can fetch currentUser with different relationships.
It merges old and new relationships.
2019-08-22 15:06:11 +03:00
Vesa Luusua
3ee937757b Shared duck file: paymentMethods.duck.js 2019-08-22 15:06:06 +03:00
Jenni Nurmi
6ef5da91df Update SDK version 2019-08-22 14:32:54 +03:00
Jenni Laakso
15994f9b8b
Merge pull request #1160 from sharetribe/v3.2.1
v3.2.1
2019-08-22 10:26:49 +03:00
Jenni Nurmi
c85d3fdd00 Update changelog 2019-08-22 10:20:57 +03:00
Jenni Laakso
4f21e2c0a7
Merge pull request #1155 from sharetribe/fix-alignment-on-listing-page
Fix alignment on listing page
2019-08-19 13:33:29 +03:00
Jenni Nurmi
30661d5d67 Update changelog 2019-08-19 13:30:30 +03:00
Jenni Nurmi
a66d947838 Fix alignment on listing page 2019-08-19 13:28:19 +03:00
Vesa Luusua
10d25041b6
Merge pull request #1154 from sharetribe/fix-topbar-inbox-hydrate-bug
Rehydrate bug: existing DOM elements were populated incorrectly
2019-08-19 13:24:56 +03:00
Vesa Luusua
3bbfc631a1 Update changelog 2019-08-19 13:21:03 +03:00
Vesa Luusua
468b387499 Rehydrate bug: existing DOM elements were populated incorrectly 2019-08-19 13:19:08 +03:00
Jenni Laakso
f0fdc46ef3
Merge pull request #1151 from sharetribe/fix-changing-country-in-payout-details
Use stripe config to determine US specific fields
2019-08-19 11:07:05 +03:00
Jenni Nurmi
4bc9824400 Update changelog 2019-08-19 10:31:58 +03:00
Jenni Nurmi
a84df90ac3 Add code comment about mcc 2019-08-19 10:31:58 +03:00
Jenni Nurmi
e94c9d9409 Use stripe config to determine us specific fields 2019-08-19 10:31:53 +03:00
Jenni Laakso
1558b9c399
Merge pull request #1148 from sharetribe/update-new-translations
Update fr and es translations
2019-08-01 10:35:56 +03:00
Jenni Nurmi
e946ffa22c Update changelog 2019-08-01 10:32:39 +03:00
Jenni Nurmi
9adf213ef0 Update es.json 2019-08-01 10:28:45 +03:00
Jenni Nurmi
df524d4b89 Update fr.json 2019-08-01 10:28:26 +03:00
Jenni Laakso
4bc0f6f53f
Merge pull request #1139 from sharetribe/update-fr-translations
Update fr translations
2019-07-22 11:19:42 +03:00
Jenni Nurmi
8ffe9cd9a6 Update changelog 2019-07-22 11:12:31 +03:00
Jenni Nurmi
50e48fac84 Update translations related to payment intents to fr.json 2019-07-22 11:05:47 +03:00
Jenni Nurmi
cfdfa2ead1 Small updates to en.json 2019-07-22 11:05:12 +03:00
Vesa Luusua
61449c4d66
Merge pull request #1137 from sharetribe/v3.2.0
New release: v3.2.0
2019-07-18 18:03:32 +03:00
Vesa Luusua
ec778ece2f New release: v3.2.0 2019-07-18 18:00:54 +03:00
Vesa Luusua
bd76364769
Merge pull request #1129 from sharetribe/keyword-search-filter-desktop
Add keyword search
2019-07-18 17:49:55 +03:00
Vesa Luusua
fa79c2c650 Update changelog 2019-07-18 17:46:29 +03:00
Vesa Luusua
8cf99fd6c0 Update FilterPopup: add labelMaxWidth prop and ellipsis: 2019-07-18 17:37:47 +03:00
Vesa Luusua
fb2c11cae2 Update FieldTextInput: use defaultValue if uncontrolled 2019-07-18 17:37:47 +03:00
Jenni Nurmi
cb242413f8 Ensure that values are string before validating them 2019-07-18 17:37:47 +03:00
Jenni Nurmi
3242f01146 Add code comments about restriction tha this can't be used with origin 2019-07-18 17:37:46 +03:00
Jenni Nurmi
2485b46b30 Create KeywordFilter
Add it to desktop and mobile layout
2019-07-18 17:37:41 +03:00
Vesa Luusua
578a02039a
Merge pull request #1136 from sharetribe/temp-remove-audit
Temporarily remove audit CI job
2019-07-16 20:19:33 +03:00
Vesa Luusua
828719085e Temporarily remove audit CI job 2019-07-16 20:03:27 +03:00
Vesa Luusua
0cb982f246
Merge pull request #1135 from sharetribe/dependency-updates
Update dependencies
2019-07-16 19:58:45 +03:00
Jenni Nurmi
468821c9e0 Update changelog 2019-07-12 16:15:42 +03:00
Jenni Nurmi
b945c98463 Update react-router-dom to 5.0.1 2019-07-12 16:09:31 +03:00
Jenni Nurmi
62d67275c3 Update react-intl to 2.9.0 2019-07-12 16:08:30 +03:00
Jenni Nurmi
138fbaa64d Update Inquirer to 6.5.0 2019-07-12 16:06:32 +03:00
Jenni Nurmi
37ebb6b4d6 Update decimal.js to 10.2.0 2019-07-12 16:05:51 +03:00
Jenni Nurmi
8825c166ed Update bfj to 7.0.1 2019-07-12 16:04:55 +03:00
Jenni Nurmi
be310ae251 Update core-js to 3.1.4 2019-07-12 14:30:39 +03:00
Jenni Nurmi
89d46e6487 Update lodash to 4.17.14 2019-07-12 14:30:39 +03:00
Jenni Laakso
423a9200cd
Merge pull request #1131 from sharetribe/ensure-booking-transition-exists
Ensure booking transition exists
2019-07-08 13:47:58 +03:00
Jenni Nurmi
cd525d0708 Update changelog and package.json 2019-07-08 13:46:00 +03:00
Vesa Luusua
f9dae4bac4 TransactionPanel: has correct next transition for enquiry 2019-07-05 17:51:58 +03:00
Vesa Luusua
a3898f6341 TransactionPage.duck: Fetch process transitions on loadData 2019-07-05 17:50:24 +03:00
Vesa Luusua
3eb5f16544 Fix proptype validation for BookingPanel 2019-07-05 17:49:25 +03:00
Vesa Luusua
3791a2e5d4
Merge pull request #1128 from sharetribe/v3.1.0
New release: v3.1.0
2019-07-05 15:50:27 +03:00
Vesa Luusua
5f113608b4 New release: v3.1.0 2019-07-05 15:34:00 +03:00
Vesa Luusua
625d87cf6b
Merge pull request #1124 from sharetribe/fix-heroButton-search-param-bug
fix heroButton search param bug
2019-07-05 15:31:28 +03:00
Vesa Luusua
11dfaff42e Update changelog 2019-07-05 15:29:16 +03:00
Vesa Luusua
5d559e2aa0 Update test 2019-07-05 15:25:54 +03:00
Janne Koivistoinen
c569fe2c51
fix heroButton search param bug
Removed the extra "s?" from the "to" parameter.
2019-07-04 16:34:53 +03:00
Jenni Laakso
ed9ad72b02
Merge pull request #1122 from sharetribe/add-singapore-to-stripe-countries
Add Singapore to supported Stripe countries
2019-07-04 15:15:36 +03:00
Jenni Nurmi
30c8e09cac Update changelog 2019-07-04 15:06:18 +03:00
Jenni Nurmi
6f4e93bbdc Fix bug in passing the id number to Stripe 2019-07-04 15:05:46 +03:00
Jenni Nurmi
426a12288b Add Singapore to Stripe countries 2019-07-04 15:05:46 +03:00
Jenni Laakso
8e550d8392
Merge pull request #1123 from sharetribe/edit-csp
Add events.mapbox.com to connect-src
2019-07-04 15:05:04 +03:00
Jenni Nurmi
841b45c169 Update changelog 2019-07-04 14:58:42 +03:00
Jenni Nurmi
669006015a Add events.mapbox.com to connect-src 2019-07-04 11:16:41 +03:00
Jenni Laakso
18492a2e44
Merge pull request #1121 from sharetribe/automatic-email-verification
Verify email automatically and redirect to landing page
2019-07-03 15:41:26 +03:00
Jenni Nurmi
ecc8657b0d Update changelog 2019-07-03 15:36:05 +03:00
Jenni Nurmi
a8f746a59f Verify email automatically and redirect to landing page 2019-07-03 15:18:57 +03:00
286 changed files with 8998 additions and 3928 deletions

View file

@ -99,4 +99,3 @@ workflows:
- format
- test
- build
- audit

View file

@ -14,6 +14,195 @@ way to update this template, but currently, we follow a pattern:
## Upcoming version 2019-XX-XX
## [v3.5.1] 2019-09-16
- [add] add orverriding function `onAdd` and `onRemove` for `CustomOverlayView` in
`SearchMapWithGoogleMap` to abide to React rules and do not `unmountComponentAtNode` when a
component is rendered by React and use `appendChild` on `onAdd` instead of `draw` to
[improve performance](https://github.com/tomchentw/react-google-maps/issues/817).
[#1200](https://github.com/sharetribe/flex-template-web/pull/1200)
- [fix] fix `CustomOverlayView` in `SearchMapWithGoogleMap` to work with new `react-intl` version,
overriding `render` method to render child object by using `createPortal` instead of
`unstable_renderSubtreeIntoContainer`.
[#1200](https://github.com/sharetribe/flex-template-web/pull/1200)
## [v3.5.0] 2019-08-29
- [change] Change the design of `BookingBreakdown` and add options to show only dates or booking
date and time there. [#1195](https://github.com/sharetribe/flex-template-web/pull/1195)
- [change] Move `BookingTimeInfo` to separate component from `InboxPage`. Add options to show only
booking dates or booking dates and times.
[#1194](https://github.com/sharetribe/flex-template-web/pull/1194)
- [add] Add new Spanish translations related to storing payment card.
[#1193](https://github.com/sharetribe/flex-template-web/pull/1193)
- [fix] Update yarn.lock (there was Lodash version resolution missing)
[#1190](https://github.com/sharetribe/flex-template-web/pull/1190)
[v3.5.0]: https://github.com/sharetribe/flex-template-web/compare/v3.4.0...v3.5.0
## [v3.4.0] 2019-08-29
- [change] Update `react-intl` to 3.1.13. More information about the changes can be found from
[Upgrade guide for react-intl@3.x](https://github.com/formatjs/react-intl/blob/master/docs/Upgrade-Guide.md)
- Proptype `intlShape` was removed so we needed to create it again. Because of this we added a new
`util/reactIntl.js` file. This file is now used to wrap all the react-intl related imports.
- `addLocaleDate` function was removed and react-intl library is now relying on native Intl APIs:
[Intl.PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/PluralRules)
and
[Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RelativeTimeFormat).
In order to support older browsers we needed to add `intl-pluralrules` and
`intl-relativetimeformat` to `util/polyfills.js`
- Also Node must be now compiled with `full-icu` which caused changes to `start` and `test`
scripts in `package.json`. We also needed to add a specific config for `nodemon`
- Default `textComponent`in `IntlProvider` changed to `React.Fragment` so we need to explicitly
set `textComponent` to `span`. Otherwise all the snapshots would have changed and it might
affect to UI if there is styles added to these spans generally in customization projects.
Note: `FormattedMessage` component now supports
[`tagName` prop](https://github.com/formatjs/react-intl/blob/master/docs/Components.md#formattedmessage)
and
[improved rich-text formatting](https://github.com/formatjs/react-intl/blob/master/docs/Components.md#rich-text-formatting).
[#1181](https://github.com/sharetribe/flex-template-web/pull/1181)
- [change] Update helmet (v3.20.0 > v3.20.1).
[#1186](https://github.com/sharetribe/flex-template-web/pull/1186)
- [fix] Lodash vulnerability: enforce newer version for react-google-maps and react-dates
[#1188](https://github.com/sharetribe/flex-template-web/pull/1188)
- [change] Update `React`, `react-test-renderer` and `react-dom` to 16.9.0. After these updates old
lifecycle methods `componentWillMount`, `componentWillUpdate` and `componentWillUpdate` will cause
deprecation warnings. Check the updated components from the PR
[#1172](https://github.com/sharetribe/flex-template-web/pull/1172)
- [fix] ProfileSettingsForm: clear correct timeout.
[#1185](https://github.com/sharetribe/flex-template-web/pull/1185)
- [fix] `availabilityPlan` prop in `EditListingAvailabilityForm` was missing.
[#1183](https://github.com/sharetribe/flex-template-web/pull/1183)
- [fix] Bug fix: valueFromForm prop wasn't passed through different subcomponents.
[#1182](https://github.com/sharetribe/flex-template-web/pull/1182)
- [add] Update German and French translations.
[#1184](https://github.com/sharetribe/flex-template-web/pull/1184)
- [change] Migrate from `react-helmet` to `react-helmet-async`
[#1179](https://github.com/sharetribe/flex-template-web/pull/1179)
- [change] Use `sanitize.css` from own file instead of npm package because updating it accidentally
might break the UI. [#1177](https://github.com/sharetribe/flex-template-web/pull/1177)
- [fix] Change app.test.js after `react-redux` update
[#1178](https://github.com/sharetribe/flex-template-web/pull/1178)
- [change] Update `react-redux`: v5.1.1 -> v7.1.1
[#1176](https://github.com/sharetribe/flex-template-web/pull/1176)
- [change] Update `seedrandom` from v2.4.4 to v3.0.3
[#1175](https://github.com/sharetribe/flex-template-web/pull/1175)
- [change] Update `inquirer` from v6.5.0 to v7.0.0
[#1174](https://github.com/sharetribe/flex-template-web/pull/1174)
- [change] Update final-form, final-form-arrays, react-final-form and react-final-form-arrays. This
forced to make some code changes:
- Old recommendation of by-passing default field formatting or parsin isn't accepted anymore
- `format={null}` => use identity function instead: `format={v => v}`
- `parse={null}` => use identity function instead: `parse={v => v}`
- Final Form passes input props (name, value, onChange, onBlur, etc. ) grouped inside input key
- those props now include `type` attribute too.
- We had old form naming pattern with prop 'form', which now conflicted with updated Final Form
(The 'form' prop was used when Redux-Form was the form library)
[#1173](https://github.com/sharetribe/flex-template-web/pull/1173)
- [change] Update `react-dates` from v18.5.0 to v20.3.0
[#1171](https://github.com/sharetribe/flex-template-web/pull/1171)
- [change] Update Prettier to v1.18.2
[#1170](https://github.com/sharetribe/flex-template-web/pull/1170)
- [change] Update `path-to-regexp` to v3.0.0
[#1169](https://github.com/sharetribe/flex-template-web/pull/1169)
- [change] Update `sharetribe-scripts` to v3.1.1
[#1167](https://github.com/sharetribe/flex-template-web/pull/1167)
- [fix] Small change to remove card tect on `SavedCardDetails` modal.
[#1166](https://github.com/sharetribe/flex-template-web/pull/1166)
- [change] Update Sentry (@sentry/browser / @sentry/node) from v4.5.1 to v5.6.2
[#1164](https://github.com/sharetribe/flex-template-web/pull/1164)
- Update dependecies: all the easily updateable minor and batch updates: array.prototype.find,
babel-jest, core-js, enzyme (et al.), express, helmet, inquirer, lodash, nodemon, raf, redux,
source-map-support [#1163](https://github.com/sharetribe/flex-template-web/pull/1163)
[v3.4.0]: https://github.com/sharetribe/flex-template-web/compare/v3.3.0...v3.4.0
## [v3.3.0] 2019-08-22
- [add] Saving payment card after payment or without initial payment. This release contains quite a
lot changes to many files. This includes:
- UI changes to `CheckoutPage` for showing the saved payment method
- One more step to `handlePaymentIntent` flow on `CheckoutPage` if the user decides to save the
payment card
- Showing error notification on `TransactionPage` if saving the payment method has failed
- Use Flex SDK v1.5.0 which has new endpoints for creating Stripe Customer and using Stripe
SetupIntents
- Add `handleCardSetup` function to `stripe.duck.js`
- New shared duck file `paymentMethods.duck.js` for handling saving, deleting and replacing the
payment method
- New page `PaymentMethodsPage` in user's account settings
- `StripePaymenAddress` used in `StripePaymentForm` is now a separate component used also in new
`PaymentMethodsForm`
- New `LayoutWrapperAccountSettingsSideNav` component which is used in account settings pages:
`ContactDetailsPage`, `PasswordChangePage`, `PayoutPreferencesPage`, `PaymentMethodsPage`
[#1138](https://github.com/sharetribe/flex-template-web/pull/1138)
Read more from Flex docs:
[How saving payment card works in FTW](https://www.sharetribe.com/docs/background/save-payment-card/)
[v3.3.0]: https://github.com/sharetribe/flex-template-web/compare/v3.2.1...v3.3.0
## [v3.2.1] 2019-08-22
- [fix] On `ListingPage` align avatar with the left side of the content and fix content width so
that it aligns with the header image.
[#1155](https://github.com/sharetribe/flex-template-web/pull/1155)
- [fix] Rehydrate bug: existing DOM elements were populated incorrectly
[#1154](https://github.com/sharetribe/flex-template-web/pull/1154)
- [fix] Don't send personal id number or business profile to Stripe API when creating a Stripe
customer if they are not required in `stripe-config.js`. This happened e.g. if someone filled the
form after selecting the US and then before sending changed the country to Finland.
[#1151](https://github.com/sharetribe/flex-template-web/pull/1151)
- [add] Add new French and Spanish translations related to keyword search and Spanish translations
related to payment intents. [#1148](https://github.com/sharetribe/flex-template-web/pull/1148)
- [add] Add new French translations related to payment intents. Also few small changes to en.json
for consistency. [#1139](https://github.com/sharetribe/flex-template-web/pull/1139)
[v3.2.1]: https://github.com/sharetribe/flex-template-web/compare/v3.2.0...v3.2.1
## [v3.2.0] 2019-07-08
- [add] Keyword search/filter added to SearchPage component.
[#1129](https://github.com/sharetribe/flex-template-web/pull/1129)
- [fix] temporarily remove audit CI job.
[#1136](https://github.com/sharetribe/flex-template-web/pull/1136)
- [change] Update outdated dependencies. This includes updating lodash to fix the security issue.
[#1135](https://github.com/sharetribe/flex-template-web/pull/1135)
[v3.2.0]: https://github.com/sharetribe/flex-template-web/compare/v3.1.1...v3.2.0
## [v3.1.1] 2019-07-08
- [fix] Ensure on `TransactionPanel` that enquiry has a correct transition when a customer tries to
book the listing. This might happen with transaction process changes (e.g. when changing from
previous default to SCA process).
[#1131](https://github.com/sharetribe/flex-template-web/pull/1131)
[v3.1.1]: https://github.com/sharetribe/flex-template-web/compare/v3.1.0...v3.1.1
## [v3.1.0] 2019-07-05
- [fix] SectionHero: fix type in search params. There was an extra "/s?".
[#1124](https://github.com/sharetribe/flex-template-web/pull/1124)
- [add] Add support for Singapore as the payout country of a provider. Also fix a bug in passing the
personal ID number to Stripe. [#1122](https://github.com/sharetribe/flex-template-web/pull/1122)
- [add] Add events.mapbox.com to `connect-src` in `csp.js` file.
[#1123](https://github.com/sharetribe/flex-template-web/pull/1123)
- [change] Verify email automatically once the verification link is clicked. Redirect the user to
the landing page after verification.
[#1121](https://github.com/sharetribe/flex-template-web/pull/1121)
[v3.0.0]: https://github.com/sharetribe/flex-template-web/compare/v3.0.0...v3.1.0
## [v3.0.0] 2019-07-02
- [add] Strong Customer Authentication (SCA) with Stripe's new PaymentIntents flow. This is a big

View file

@ -56,6 +56,10 @@ See the Flex Docs site: https://www.sharetribe.com/docs/
See also the [docs/](docs/) directory for some additional internal documentation.
## Get help join Sharetribe Flex Developer Slack channel
If you have any questions about development, the best place to ask them is the Flex Developer Slack channel at https://www.sharetribe.com/flex-slack
## License
This project is licensed under the terms of the Apache-2.0 license.

View file

@ -1,12 +1,13 @@
{
"name": "app",
"version": "3.0.0",
"version": "3.5.1",
"private": true,
"license": "Apache-2.0",
"dependencies": {
"@formatjs/intl-relativetimeformat": "^2.8.2",
"@mapbox/polyline": "^1.0.0",
"@sentry/browser": "4.5.1",
"@sentry/node": "4.5.1",
"@sentry/browser": "5.6.2",
"@sentry/node": "5.6.2",
"array-includes": "^3.0.3",
"array.prototype.find": "^2.0.4",
"autosize": "^4.0.0",
@ -15,59 +16,67 @@
"classnames": "^2.2.6",
"compression": "^1.7.4",
"cookie-parser": "^1.4.4",
"core-js": "^2.5.7",
"decimal.js": "10.0.1",
"core-js": "^3.1.4",
"decimal.js": "10.2.0",
"dotenv": "6.2.0",
"dotenv-expand": "4.2.0",
"express": "^4.16.4",
"express-enforces-ssl": "^1.1.0",
"express-sitemap": "^1.8.0",
"final-form": "^4.11.0",
"final-form-arrays": "^1.1.0",
"final-form": "^4.18.5",
"final-form-arrays": "^3.0.0",
"full-icu": "^1.3.0",
"helmet": "^3.18.0",
"lodash": "^4.17.11",
"intl-pluralrules": "^1.0.3",
"lodash": "^4.17.14",
"mapbox-gl-multitouch": "^1.0.3",
"moment": "^2.22.2",
"object.entries": "^1.0.4",
"object.values": "^1.0.4",
"path-to-regexp": "^2.4.0",
"path-to-regexp": "^3.0.0",
"prop-types": "^15.7.2",
"query-string": "^5.1.1",
"raf": "3.4.0",
"react": "^16.8.6",
"react-dates": "^18.5.0",
"react-dom": "^16.8.6",
"react-final-form": "^4.0.2",
"react-final-form-arrays": "^2.0.1",
"raf": "^3.4.0",
"react": "^16.9.0",
"react-dates": "^20.3.0",
"react-dom": "^16.9.0",
"react-final-form": "^6.3.0",
"react-final-form-arrays": "^3.1.1",
"react-google-maps": "^9.4.5",
"react-helmet": "^5.2.0",
"react-intl": "^2.7.2",
"react-helmet-async": "^1.0.2",
"react-intl": "^3.1.13",
"react-moment-proptypes": "^1.6.0",
"react-redux": "^5.1.1",
"react-router-dom": "^4.3.0",
"react-redux": "^7.1.1",
"react-router-dom": "^5.0.1",
"redux": "^4.0.1",
"redux-thunk": "^2.3.0",
"sanitize.css": "^5.0.0",
"seedrandom": "^2.4.4",
"sharetribe-flex-sdk": "^1.4.1",
"sharetribe-scripts": "3.0.0",
"seedrandom": "^3.0.3",
"sharetribe-flex-sdk": "^1.5.0",
"sharetribe-scripts": "3.1.1",
"smoothscroll-polyfill": "^0.4.0",
"source-map-support": "^0.5.9",
"url": "^0.11.0"
},
"devDependencies": {
"babel-jest": "24.7.1",
"bfj": "^6.1.1",
"babel-jest": "24.9.0",
"bfj": "^7.0.1",
"chalk": "^2.4.1",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.12.0",
"enzyme-to-json": "^3.3.5",
"inquirer": "^6.3.0",
"inquirer": "^7.0.0",
"nodemon": "^1.17.2",
"prettier": "^1.15.3"
"prettier": "^1.18.2"
},
"resolutions": {
"react-test-renderer": "^16.8.6"
"react-dates/lodash": "^4.17.14",
"react-google-maps/lodash": "^4.17.14",
"react-test-renderer": "^16.9.0"
},
"nodemonConfig": {
"execMap": {
"js": "node --icu-data-dir=node_modules/full-icu"
}
},
"scripts": {
"audit": "yarn audit --json | node scripts/audit.js",
@ -78,10 +87,10 @@
"format": "prettier --write '**/*.{js,css}'",
"format-ci": "prettier --list-different '**/*.{js,css}'",
"format-docs": "prettier --write '**/*.md'",
"test": "sharetribe-scripts test",
"test": "NODE_ICU_DATA=node_modules/full-icu sharetribe-scripts test",
"test-ci": "sharetribe-scripts test --runInBand",
"eject": "sharetribe-scripts eject",
"start": "node server/index.js",
"start": "node --icu-data-dir=node_modules/full-icu server/index.js",
"dev-server": "export NODE_ENV=development PORT=4000 REACT_APP_CANONICAL_ROOT_URL=http://localhost:4000&&yarn run build&&nodemon --watch server server/index.js",
"heroku-postbuild": "yarn run build",
"translate": "node scripts/translations.js"

View file

@ -88,6 +88,7 @@
</head>
<body>
<div id="root"><!--!body--></div>
<div id="portal-root"></div>
<script>
(function(document){
// Check if Promise exists in window

View file

@ -23,6 +23,7 @@ const defaultDirectives = {
'maps.googleapis.com',
'*.tiles.mapbox.com',
'api.mapbox.com',
'events.mapbox.com',
// Google Analytics
'www.google-analytics.com',

View file

@ -51,6 +51,17 @@ exports.errorHandler = () => {
}
};
const responseAPIErrors = error => {
return error && error.data && error.data.errors ? error.data.errors : [];
};
const responseApiErrorInfo = err =>
responseAPIErrors(err).map(e => ({
status: e.status,
code: e.code,
meta: e.meta,
}));
/**
* Logs a error. If Sentry client is set up
* passes the error to that. Otherwise prints

View file

@ -78,12 +78,12 @@ class RouteComponentRenderer extends Component {
handleLocationChanged(this.props.dispatch, this.props.location);
}
componentWillReceiveProps(nextProps) {
componentDidUpdate() {
// Calling loadData after initial rendering (on client side).
// This makes it possible to use loadData as default client side data loading technique.
// However it is better to fetch data before location change to avoid "Loading data" state.
callLoadData(nextProps);
handleLocationChanged(nextProps.dispatch, nextProps.location);
callLoadData(this.props);
handleLocationChanged(this.props.dispatch, this.props.location);
}
render() {

View file

@ -6,13 +6,13 @@ import ReactDOMServer from 'react-dom/server';
// https://github.com/airbnb/react-dates#initialize
// NOTE: Initializing it here will initialize it also for app.test.js
import 'react-dates/initialize';
import Helmet from 'react-helmet';
import { HelmetProvider } from 'react-helmet-async';
import { BrowserRouter, StaticRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import difference from 'lodash/difference';
import mapValues from 'lodash/mapValues';
import moment from 'moment';
import { IntlProvider, addLocaleData } from 'react-intl';
import { IntlProvider } from './util/reactIntl';
import configureStore from './store';
import routeConfiguration from './routeConfiguration';
import Routes from './Routes';
@ -23,24 +23,20 @@ import defaultMessages from './translations/en.json';
// If you want to change the language, change the imports to match the wanted locale:
// 1) Change the language in the config.js file!
// 2) Import correct locale rules for React Intl library
// 3) Import correct locale rules for Moment library
// 4) Use the `messagesInLocale` import to add the correct translation file.
// 2) Import correct locale rules for Moment library
// 3) Use the `messagesInLocale` import to add the correct translation file.
// 4) To support older browsers we need add the correct locale for intl-relativetimeformat to `util/polyfills.js`
// Note that there is also translations in './translations/countryCodes.js' file
// This file contains ISO 3166-1 alpha-2 country codes, country names and their translations in our default languages
// This used to collect billing address in StripePaymentAddress on CheckoutPage
// Step 2:
// Import locale rules for React Intl library
import localeData from 'react-intl/locale-data/en';
// Step 3:
// If you are using a non-english locale with moment library,
// you should also import time specific formatting rules for that locale
// e.g. for French: import 'moment/locale/fr';
// Step 4:
// Step 3:
// If you are using a non-english locale, point `messagesInLocale` to correct .json file
import messagesInLocale from './translations/fr.json';
@ -75,13 +71,12 @@ const localeMessages = isTestEnv ? testMessages : messages;
const setupLocale = () => {
if (isTestEnv) {
// Don't change the locale in tests
// Use english as a default locale in tests
// This affects app.test.js and app.node.test.js tests
config.locale = 'en';
return;
}
// Add the translation messages
addLocaleData([...localeData]);
// Set the Moment locale globally
// See: http://momentjs.com/docs/#/i18n/changing-locale/
moment.locale(config.locale);
@ -91,11 +86,13 @@ export const ClientApp = props => {
const { store } = props;
setupLocale();
return (
<IntlProvider locale={config.locale} messages={localeMessages}>
<IntlProvider locale={config.locale} messages={localeMessages} textComponent="span">
<Provider store={store}>
<BrowserRouter>
<Routes routes={routeConfiguration()} />
</BrowserRouter>
<HelmetProvider>
<BrowserRouter>
<Routes routes={routeConfiguration()} />
</BrowserRouter>
</HelmetProvider>
</Provider>
</IntlProvider>
);
@ -106,14 +103,17 @@ const { any, string } = PropTypes;
ClientApp.propTypes = { store: any.isRequired };
export const ServerApp = props => {
const { url, context, store } = props;
const { url, context, helmetContext, store } = props;
setupLocale();
HelmetProvider.canUseDOM = false;
return (
<IntlProvider locale={config.locale} messages={localeMessages}>
<IntlProvider locale={config.locale} messages={localeMessages} textComponent="span">
<Provider store={store}>
<StaticRouter location={url} context={context}>
<Routes routes={routeConfiguration()} />
</StaticRouter>
<HelmetProvider context={helmetContext}>
<StaticRouter location={url} context={context}>
<Routes routes={routeConfiguration()} />
</StaticRouter>
</HelmetProvider>
</Provider>
</IntlProvider>
);
@ -137,9 +137,11 @@ export const renderApp = (url, serverContext, preloadedState) => {
// shouldn't do any SDK calls in the (server) rendering lifecycle.
const store = configureStore(preloadedState);
const helmetContext = {};
const body = ReactDOMServer.renderToString(
<ServerApp url={url} context={serverContext} store={store} />
<ServerApp url={url} context={serverContext} helmetContext={helmetContext} store={store} />
);
const head = Helmet.renderStatic();
const { helmet: head } = helmetContext;
return { head, body };
};

96
src/app.node.test.js Normal file
View file

@ -0,0 +1,96 @@
/**
* @jest-environment node
*/
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOMServer from 'react-dom/server';
import { Helmet } from 'react-helmet-async';
import forEach from 'lodash/forEach';
import { ClientApp, ServerApp } from './app';
import configureStore from './store';
const render = (url, context) => {
const store = configureStore();
const helmetContext = {};
const body = ReactDOMServer.renderToString(
<ServerApp url={url} context={context} helmetContext={helmetContext} store={store} />
);
const { helmet: head } = helmetContext;
return { head, body };
};
describe('Application - node environment', () => {
it('renders in the server without crashing', () => {
render('/', {});
});
it('renders the styleguide without crashing', () => {
render('/styleguide', {});
});
it('server renders pages that do not require authentication', () => {
const urlTitles = {
'/': 'LandingPage.schemaTitle',
'/s': 'SearchPage.schemaTitle',
'/l/listing-title-slug/1234': 'ListingPage.loadingListingTitle',
'/l/1234': 'ListingPage.loadingListingTitle',
'/u/1234': 'ProfilePage.schemaTitle',
'/login': 'AuthenticationPage.schemaTitleLogin',
'/signup': 'AuthenticationPage.schemaTitleSignup',
'/recover-password': 'PasswordRecoveryPage.title',
'/this-url-should-not-be-found': 'NotFoundPage.title',
'/reset-password?t=token&e=email': 'PasswordResetPage.title',
};
forEach(urlTitles, (title, url) => {
const context = {};
const { head, body } = render(url, context);
expect(head.title.toString()).toContain(title);
// context.url will contain the URL to redirect to if a <Redirect> was used
expect(context.url).not.toBeDefined();
});
});
it('server renders redirects for pages that require authentication', () => {
const loginPath = '/login';
const signupPath = '/signup';
const urlRedirects = {
'/l/new': signupPath,
'/l/listing-title-slug/1234/new/description': signupPath,
'/l/listing-title-slug/1234/checkout': signupPath,
'/profile-settings': loginPath,
'/inbox': loginPath,
'/inbox/orders': loginPath,
'/inbox/sales': loginPath,
'/order/1234': loginPath,
'/order/1234/details': loginPath,
'/sale/1234': loginPath,
'/sale/1234/details': loginPath,
'/listings': loginPath,
'/account': loginPath,
'/account/contact-details': loginPath,
'/account/change-password': loginPath,
'/account/payments': loginPath,
'/verify-email': loginPath,
};
forEach(urlRedirects, (redirectPath, url) => {
const context = {};
const { body } = render(url, context);
expect(context.url).toEqual(redirectPath);
});
});
it('redirects to correct URLs', () => {
const urlRedirects = { '/l': '/', '/u': '/' };
forEach(urlRedirects, (redirectPath, url) => {
const context = {};
const { body } = render(url, context);
expect(context.url).toEqual(redirectPath);
});
});
});

View file

@ -1,20 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOMServer from 'react-dom/server';
import Helmet from 'react-helmet';
import forEach from 'lodash/forEach';
import { ClientApp, ServerApp } from './app';
import { ClientApp } from './app';
import configureStore from './store';
const render = (url, context) => {
const store = configureStore();
const body = ReactDOMServer.renderToString(
<ServerApp url={url} context={context} store={store} />
);
const head = Helmet.peek();
return { head, body };
};
const jsdomScroll = window.scroll;
beforeAll(() => {
// Mock window.scroll - otherwise, Jest/JSDOM will print a not-implemented error.
@ -25,7 +13,7 @@ afterAll(() => {
window.scroll = jsdomScroll;
});
describe('Application', () => {
describe('Application - JSDOM environment', () => {
it('renders in the client without crashing', () => {
window.google = { maps: {} };
const store = configureStore();
@ -33,74 +21,4 @@ describe('Application', () => {
ReactDOM.render(<ClientApp store={store} />, div);
delete window.google;
});
it('renders in the server without crashing', () => {
render('/', {});
});
it('renders the styleguide without crashing', () => {
render('/styleguide', {});
});
it('server renders pages that do not require authentication', () => {
const urlTitles = {
'/': 'LandingPage.schemaTitle',
'/s': 'SearchPage.schemaTitle',
'/l/listing-title-slug/1234': 'ListingPage.loadingListingTitle',
'/l/1234': 'ListingPage.loadingListingTitle',
'/u/1234': 'ProfilePage.schemaTitle',
'/login': 'AuthenticationPage.schemaTitleLogin',
'/signup': 'AuthenticationPage.schemaTitleSignup',
'/recover-password': 'PasswordRecoveryPage.title',
'/this-url-should-not-be-found': 'NotFoundPage.title',
'/reset-password?t=token&e=email': 'PasswordResetPage.title',
};
forEach(urlTitles, (title, url) => {
const context = {};
const { head, body } = render(url, context);
expect(head.title).toContain(title);
// context.url will contain the URL to redirect to if a <Redirect> was used
expect(context.url).not.toBeDefined();
});
});
it('server renders redirects for pages that require authentication', () => {
const loginPath = '/login';
const signupPath = '/signup';
const urlRedirects = {
'/l/new': signupPath,
'/l/listing-title-slug/1234/new/description': signupPath,
'/l/listing-title-slug/1234/checkout': signupPath,
'/profile-settings': loginPath,
'/inbox': loginPath,
'/inbox/orders': loginPath,
'/inbox/sales': loginPath,
'/order/1234': loginPath,
'/order/1234/details': loginPath,
'/sale/1234': loginPath,
'/sale/1234/details': loginPath,
'/listings': loginPath,
'/account': loginPath,
'/account/contact-details': loginPath,
'/account/change-password': loginPath,
'/account/payments': loginPath,
'/verify-email': loginPath,
};
forEach(urlRedirects, (redirectPath, url) => {
const context = {};
const { body } = render(url, context);
expect(context.url).toEqual(redirectPath);
});
});
it('redirects to correct URLs', () => {
const urlRedirects = { '/l': '/', '/u': '/' };
forEach(urlRedirects, (redirectPath, url) => {
const context = {};
const { body } = render(url, context);
expect(context.url).toEqual(redirectPath);
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
src/assets/ideas-image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

BIN
src/assets/ideas.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

593
src/assets/sanitize.css Normal file
View file

@ -0,0 +1,593 @@
/*! sanitize.css v5.0.0 | CC0 License | github.com/jonathantneal/sanitize.css */
/* Document (https://html.spec.whatwg.org/multipage/semantics.html#semantics)
========================================================================== */
/**
* 1. Remove repeating backgrounds in all browsers (opinionated).
* 2. Add box sizing inheritence in all browsers (opinionated).
*/
*,
::before,
::after {
background-repeat: no-repeat; /* 1 */
box-sizing: inherit; /* 2 */
}
/**
* 1. Add text decoration inheritance in all browsers (opinionated).
* 2. Add vertical alignment inheritence in all browsers (opinionated).
*/
::before,
::after {
text-decoration: inherit; /* 1 */
vertical-align: inherit; /* 2 */
}
/**
* 1. Add border box sizing in all browsers (opinionated).
* 2. Add the default cursor in all browsers (opinionated).
* 3. Prevent font size adjustments after orientation changes in IE and iOS.
*/
html {
box-sizing: border-box; /* 1 */
cursor: default; /* 2 */
-ms-text-size-adjust: 100%; /* 3 */
-webkit-text-size-adjust: 100%; /* 3 */
}
/* Sections (https://html.spec.whatwg.org/multipage/semantics.html#sections)
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
background: pink;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content (https://html.spec.whatwg.org/multipage/semantics.html#grouping-content)
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main {
/* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* Remove the list style on navigation lists in all browsers (opinionated).
*/
nav ol,
nav ul {
list-style: none;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics (https://html.spec.whatwg.org/multipage/semantics.html#text-level-semantics)
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* 1. Remove the bottom border in Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ffff00;
color: #000000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
* Remove the text shadow on text selections (opinionated).
* 1. Restore the coloring undone by defining the text shadow (opinionated).
*/
::-moz-selection {
background-color: #b3d4fc; /* 1 */
color: #000000; /* 1 */
text-shadow: none;
}
::selection {
background-color: #b3d4fc; /* 1 */
color: #000000; /* 1 */
text-shadow: none;
}
/* Embedded content (https://html.spec.whatwg.org/multipage/embedded-content.html#embedded-content)
========================================================================== */
/*
* Change the alignment on media elements in all browers (opinionated).
*/
audio,
canvas,
iframe,
img,
svg,
video {
vertical-align: middle;
}
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Change the fill color to match the text color in all browsers (opinionated).
*/
svg {
fill: currentColor;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Tabular data (https://html.spec.whatwg.org/multipage/tables.html#tables)
========================================================================== */
/**
* Collapse border spacing
*/
table {
border-collapse: collapse;
}
/* Forms (https://html.spec.whatwg.org/multipage/forms.html#forms)
========================================================================== */
/**
* Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
margin: 0;
}
/**
* Inherit styling in all browsers (opinionated).
*/
button,
input,
select,
textarea {
background-color: transparent;
color: inherit;
font-size: inherit;
line-height: inherit;
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type='button']:-moz-focusring,
[type='reset']:-moz-focusring,
[type='submit']:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* 1. Remove the default vertical scrollbar in IE.
* 2. Change the resize direction on textareas in all browsers (opinionated).
*/
textarea {
overflow: auto; /* 1 */
resize: vertical; /* 2 */
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type='checkbox'],
[type='radio'] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type='search']::-webkit-search-cancel-button,
[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive elements (https://html.spec.whatwg.org/multipage/forms.html#interactive-elements)
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting (https://html.spec.whatwg.org/multipage/scripting.html#scripting-3)
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* User interaction (https://html.spec.whatwg.org/multipage/interaction.html#editing)
========================================================================== */
/*
* Remove the tapping delay on clickable elements (opinionated).
* 1. Remove the tapping delay in IE 10.
*/
a,
area,
button,
input,
label,
select,
summary,
textarea,
[tabindex] {
-ms-touch-action: manipulation; /* 1 */
touch-action: manipulation;
}
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}
/* ARIA (https://w3c.github.io/html-aria/)
========================================================================== */
/**
* Change the cursor on busy elements (opinionated).
*/
[aria-busy='true'] {
cursor: progress;
}
/*
* Change the cursor on control elements (opinionated).
*/
[aria-controls] {
cursor: pointer;
}
/*
* Change the display on visually hidden accessible elements (opinionated).
*/
[aria-hidden='false'][hidden]:not(:focus) {
clip: rect(0, 0, 0, 0);
display: inherit;
position: absolute;
}
/*
* Change the cursor on disabled, not-editable, or otherwise
* inoperable elements (opinionated).
*/
[aria-disabled] {
cursor: default;
}

View file

@ -1,6 +1,6 @@
import React from 'react';
import { string, arrayOf, bool, func, number } from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl';
import dropWhile from 'lodash/dropWhile';
import classNames from 'classnames';
import { Avatar, InlineTextButton, ReviewRating, UserDisplayName } from '../../components';

View file

@ -1,6 +1,6 @@
import React from 'react';
import { string, oneOfType, bool } from 'prop-types';
import { injectIntl, intlShape } from 'react-intl';
import { injectIntl, intlShape } from '../../util/reactIntl';
import classNames from 'classnames';
import { propTypes } from '../../util/types';
import {

View file

@ -11,20 +11,11 @@
.lineItem {
@apply --marketplaceH4FontStyles;
/* Since the line items have different font sizes in the label and the
value columns, we have to balance the line to align to the
baseline. */
margin: -1px 0 0 0;
margin: 0;
@media (--viewportMedium) {
margin: -1px 0 8px 0;
&:last-of-type {
/* Last item should not have bottom margin that goes to the bottom of
the component */
margin-bottom: 0;
}
padding-top: 6px;
padding-bottom: 2px;
}
display: flex;
@ -32,14 +23,48 @@
justify-content: space-between;
}
.bookingPeriod {
flex: 1 1;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
padding-bottom: 6px;
@media (--viewportMedium) {
padding-bottom: 8px;
}
}
.bookingPeriodSectionRigth {
text-align: right;
}
.dayLabel {
@apply --marketplaceH5FontStyles;
margin: 0;
color: var(--matterColorAnti);
line-height: 24px;
padding-top: 2px;
padding-bottom: 4px;
@media (--viewportMedium) {
padding-top: 1px;
padding-bottom: 7px;
}
}
.dayInfo {
@apply --marketplaceButtonFontStyles;
}
.subTotalLineItem {
@apply --marketplaceH4FontStyles;
font-weight: var(--fontWeightBold);
margin: 4px 0 1px 0;
margin: 0;
padding: 5px 0 1px 0;
@media (--viewportMedium) {
margin: 6px 0 9px 0;
padding: 6px 0 2px 0;
}
display: flex;
@ -47,72 +72,65 @@
justify-content: space-between;
}
.lineItemTotal {
@apply --marketplaceH4FontStyles;
margin: 0;
padding-top: 6px;
padding-bottom: 6px;
display: flex;
flex-direction: row;
align-items: flex-end;
justify-content: space-between;
@media (--viewportMedium) {
padding-bottom: 2px;
}
}
.itemLabel {
@apply --marketplaceSmallFontStyles;
flex: 1;
margin-top: 1px; /* align with baseline */
}
.itemValue {
@apply --marketplaceH4FontStyles;
font-weight: 600;
@apply --marketplaceSmallFontStyles;
margin: 0 0 0 10px;
@media (--viewportMedium) {
font-weight: 700;
margin: 0 0 0 10px;
}
}
.totalDivider {
/* dimensions */
width: 100%;
height: 1px;
margin: 13px 0 4px 0;
margin: 12px 0 5px 0;
border: none;
background-color: var(--matterColorNegative);
@media (--viewportMedium) {
margin: 2px 0 14px 0;
margin: 7px 0 0px 0;
}
}
.totalLabel {
font-weight: var(--fontWeightSemiBold);
/* Move so that baseline aligns with the total price */
padding-top: 6px;
@media (--viewportMedium) {
padding-top: 8px;
}
font-weight: var(--fontWeightNormal);
}
.totalPrice {
@apply --marketplaceBodyFontStyles;
font-weight: var(--fontWeightSemiBold);
@apply --marketplaceButtonFontStyles;
margin: 0 0 0 10px;
padding-top: 4px;
@media (--viewportMedium) {
margin: 2px 0 0 10px;
padding-top: 0;
}
padding-top: 0px;
}
.feeInfo {
@apply --marketplaceTinyFontStyles;
color: var(--matterColorAnti);
margin: 0 0 20px 0;
flex-shrink: 0;
margin: 0;
color: var(--matterColorAnti);
padding-top: 4px;
padding-bottom: 14px;
@media (--viewportMedium) {
margin-top: auto;
margin-bottom: 20px;
}
@media (--viewportLarge) {
margin-top: 4px;
margin-bottom: 22px;
padding-top: 11px;
padding-bottom: 21px;
}
}

View file

@ -10,7 +10,13 @@ import {
TRANSITION_CONFIRM_PAYMENT,
TX_TRANSITION_ACTOR_CUSTOMER,
} from '../../util/transaction';
import { LINE_ITEM_DAY, LINE_ITEM_NIGHT, LINE_ITEM_UNITS } from '../../util/types';
import {
LINE_ITEM_DAY,
LINE_ITEM_NIGHT,
LINE_ITEM_UNITS,
DATE_TYPE_DATE,
DATE_TYPE_DATETIME,
} from '../../util/types';
import config from '../../config';
import BookingBreakdown from './BookingBreakdown';
@ -60,6 +66,7 @@ export const Checkout = {
props: {
userRole: 'customer',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
payinTotal: new Money(9000, CURRENCY),
payoutTotal: new Money(9000, CURRENCY),
@ -86,6 +93,7 @@ export const CustomerOrder = {
props: {
userRole: 'customer',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
payinTotal: new Money(9000, CURRENCY),
payoutTotal: new Money(9000, CURRENCY),
@ -112,6 +120,7 @@ export const ProviderSale = {
props: {
userRole: 'provider',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
payinTotal: new Money(9000, CURRENCY),
payoutTotal: new Money(7000, CURRENCY),
@ -145,6 +154,7 @@ export const ProviderSaleZeroCommission = {
props: {
userRole: 'provider',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
payinTotal: new Money(9000, CURRENCY),
payoutTotal: new Money(9000, CURRENCY),
@ -178,6 +188,7 @@ export const ProviderSaleSingleNight = {
props: {
userRole: 'provider',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATE,
transaction: exampleTransaction({
payinTotal: new Money(4500, CURRENCY),
payoutTotal: new Money(2500, CURRENCY),
@ -211,6 +222,7 @@ export const ProviderSalePreauthorized = {
props: {
userRole: 'provider',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
lastTransition: TRANSITION_CONFIRM_PAYMENT,
payinTotal: new Money(4500, CURRENCY),
@ -245,6 +257,7 @@ export const ProviderSaleAccepted = {
props: {
userRole: 'provider',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
lastTransition: TRANSITION_ACCEPT,
payinTotal: new Money(4500, CURRENCY),
@ -279,6 +292,7 @@ export const ProviderSaleDeclined = {
props: {
userRole: 'provider',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
lastTransition: TRANSITION_DECLINE,
payinTotal: new Money(4500, CURRENCY),
@ -313,6 +327,7 @@ export const ProviderSaleAutoDeclined = {
props: {
userRole: 'provider',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
lastTransition: TRANSITION_EXPIRE,
payinTotal: new Money(4500, CURRENCY),
@ -347,6 +362,7 @@ export const ProviderSaleDelivered = {
props: {
userRole: 'provider',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
lastTransition: TRANSITION_COMPLETE,
payinTotal: new Money(4500, CURRENCY),
@ -381,6 +397,7 @@ export const ProviderSaleCanceled = {
props: {
userRole: 'provider',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
lastTransition: TRANSITION_CANCEL,
payinTotal: new Money(0, CURRENCY),
@ -432,6 +449,7 @@ export const SingleDay = {
props: {
userRole: 'customer',
unitType: LINE_ITEM_DAY,
dateType: DATE_TYPE_DATE,
transaction: exampleTransaction({
payinTotal: new Money(4500, CURRENCY),
payoutTotal: new Money(4500, CURRENCY),
@ -458,6 +476,7 @@ export const MultipleDays = {
props: {
userRole: 'customer',
unitType: LINE_ITEM_DAY,
dateType: DATE_TYPE_DATE,
transaction: exampleTransaction({
payinTotal: new Money(9000, CURRENCY),
payoutTotal: new Money(9000, CURRENCY),
@ -484,6 +503,7 @@ export const UnitsType = {
props: {
userRole: 'customer',
unitType: LINE_ITEM_UNITS,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
payinTotal: new Money(9000, CURRENCY),
payoutTotal: new Money(9000, CURRENCY),
@ -510,6 +530,7 @@ export const CustomPricing = {
props: {
userRole: 'customer',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
payinTotal: new Money(12800, CURRENCY),
payoutTotal: new Money(12600, CURRENCY),
@ -560,6 +581,7 @@ export const CustomPricingWithRefund = {
props: {
userRole: 'customer',
unitType: LINE_ITEM_NIGHT,
dateType: DATE_TYPE_DATETIME,
transaction: exampleTransaction({
payinTotal: new Money(0, CURRENCY),
payoutTotal: new Money(0, CURRENCY),

View file

@ -4,7 +4,7 @@
*/
import React from 'react';
import { oneOf, string } from 'prop-types';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import { FormattedMessage, intlShape, injectIntl } from '../../util/reactIntl';
import classNames from 'classnames';
import {
propTypes,
@ -12,8 +12,8 @@ import {
LINE_ITEM_PROVIDER_COMMISSION,
} from '../../util/types';
import LineItemUnitPriceMaybe from './LineItemUnitPriceMaybe';
import LineItemBookingPeriod from './LineItemBookingPeriod';
import LineItemBasePriceMaybe from './LineItemBasePriceMaybe';
import LineItemUnitsMaybe from './LineItemUnitsMaybe';
import LineItemSubTotalMaybe from './LineItemSubTotalMaybe';
import LineItemCustomerCommissionMaybe from './LineItemCustomerCommissionMaybe';
@ -27,7 +27,16 @@ import LineItemUnknownItemsMaybe from './LineItemUnknownItemsMaybe';
import css from './BookingBreakdown.css';
export const BookingBreakdownComponent = props => {
const { rootClassName, className, userRole, unitType, transaction, booking, intl } = props;
const {
rootClassName,
className,
userRole,
unitType,
transaction,
booking,
intl,
dateType,
} = props;
const isCustomer = userRole === 'customer';
const isProvider = userRole === 'provider';
@ -40,12 +49,49 @@ export const BookingBreakdownComponent = props => {
const classes = classNames(rootClassName || css.root, className);
/**
* BookingBreakdown contains different line items:
*
* LineItemBookingPeriod: prints booking start and booking end types. Prop dateType
* determines if the date and time or only the date is shown
*
* LineItemUnitsMaybe: if he unitType is line-item/unit print the name and
* quantity of the unit
*
* LineItemBasePriceMaybe: prints the base price calculation for the listing, e.g.
* "$150.00 * 2 nights $300"
*
* LineItemUnitPriceMaybe: prints just the unit price, e.g. "Price per night $32.00".
* This line item is not used by default in the BookingBreakdown.
*
* LineItemUnknownItemsMaybe: prints the line items that are unknown. In ideal case there
* should not be unknown line items. If you are using custom pricing, you should create
* new custom line items if you need them.
*
* LineItemSubTotalMaybe: prints subtotal of line items before possible
* commission or refunds
*
* LineItemRefundMaybe: prints the amount of refund
*
* LineItemCustomerCommissionMaybe: prints the amount of customer commission
* The default transaction process used by FTW doesn't include the customer commission.
*
* LineItemCustomerCommissionRefundMaybe: prints the amount of refunded customer commission
*
* LineItemProviderCommissionMaybe: prints the amount of provider commission
*
* LineItemProviderCommissionRefundMaybe: prints the amount of refunded provider commission
*
* LineItemTotalPrice: prints total price of the transaction
*
*/
return (
<div className={classes}>
<LineItemUnitPriceMaybe transaction={transaction} unitType={unitType} intl={intl} />
<LineItemBookingPeriod transaction={transaction} booking={booking} unitType={unitType} />
<LineItemBookingPeriod booking={booking} unitType={unitType} dateType={dateType} />
<LineItemUnitsMaybe transaction={transaction} unitType={unitType} />
<LineItemBasePriceMaybe transaction={transaction} unitType={unitType} intl={intl} />
<LineItemUnknownItemsMaybe transaction={transaction} intl={intl} />
<LineItemSubTotalMaybe
@ -78,8 +124,8 @@ export const BookingBreakdownComponent = props => {
intl={intl}
/>
<hr className={css.totalDivider} />
<LineItemTotalPrice transaction={transaction} isProvider={isProvider} intl={intl} />
{hasCommissionLineItem ? (
<span className={css.feeInfo}>
<FormattedMessage id="BookingBreakdown.commissionFeeNote" />
@ -89,7 +135,7 @@ export const BookingBreakdownComponent = props => {
);
};
BookingBreakdownComponent.defaultProps = { rootClassName: null, className: null };
BookingBreakdownComponent.defaultProps = { rootClassName: null, className: null, dateType: null };
BookingBreakdownComponent.propTypes = {
rootClassName: string,
@ -99,6 +145,7 @@ BookingBreakdownComponent.propTypes = {
unitType: propTypes.bookingUnitType.isRequired,
transaction: propTypes.transaction.isRequired,
booking: propTypes.booking.isRequired,
dateType: propTypes.dateType,
// from injectIntl
intl: intlShape.isRequired,

View file

@ -7,6 +7,7 @@ import {
TRANSITION_CANCEL,
TRANSITION_REQUEST_PAYMENT,
TX_TRANSITION_ACTOR_CUSTOMER,
DATE_TYPE_DATE,
} from '../../util/transaction';
import { LINE_ITEM_NIGHT } from '../../util/types';
import { BookingBreakdownComponent } from './BookingBreakdown';
@ -42,6 +43,7 @@ describe('BookingBreakdown', () => {
<BookingBreakdownComponent
userRole="customer"
unitType={LINE_ITEM_NIGHT}
dateType={DATE_TYPE_DATE}
transaction={exampleTransaction({
payinTotal: new Money(2000, 'USD'),
payoutTotal: new Money(2000, 'USD'),
@ -71,6 +73,7 @@ describe('BookingBreakdown', () => {
<BookingBreakdownComponent
userRole="customer"
unitType={LINE_ITEM_NIGHT}
dateType={DATE_TYPE_DATE}
transaction={exampleTransaction({
payinTotal: new Money(2000, 'USD'),
payoutTotal: new Money(2000, 'USD'),
@ -100,6 +103,7 @@ describe('BookingBreakdown', () => {
<BookingBreakdownComponent
userRole="provider"
unitType={LINE_ITEM_NIGHT}
dateType={DATE_TYPE_DATE}
transaction={exampleTransaction({
payinTotal: new Money(2000, 'USD'),
payoutTotal: new Money(1800, 'USD'),
@ -135,6 +139,7 @@ describe('BookingBreakdown', () => {
<BookingBreakdownComponent
userRole="provider"
unitType={LINE_ITEM_NIGHT}
dateType={DATE_TYPE_DATE}
transaction={exampleTransaction({
lastTransition: TRANSITION_CANCEL,
payinTotal: new Money(0, 'USD'),

View file

@ -0,0 +1,45 @@
import React from 'react';
import { FormattedMessage, intlShape } from '../../util/reactIntl';
import { formatMoney } from '../../util/currency';
import { LINE_ITEM_NIGHT, LINE_ITEM_DAY, propTypes } from '../../util/types';
import css from './BookingBreakdown.css';
const LineItemBasePriceMaybe = props => {
const { transaction, unitType, intl } = props;
const isNightly = unitType === LINE_ITEM_NIGHT;
const isDaily = unitType === LINE_ITEM_DAY;
const translationKey = isNightly
? 'BookingBreakdown.baseUnitNight'
: isDaily
? 'BookingBreakdown.baseUnitDay'
: 'BookingBreakdown.baseUnitQuantity';
// Find correct line-item for given unitType prop.
// It should be one of the following: 'line-item/night, 'line-item/day', 'line-item/units', or 'line-item/time'
// These are defined in '../../util/types';
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const quantity = unitPurchase ? unitPurchase.quantity.toString() : null;
const unitPrice = unitPurchase ? formatMoney(intl, unitPurchase.unitPrice) : null;
const total = unitPurchase ? formatMoney(intl, unitPurchase.lineTotal) : null;
return quantity && total ? (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<FormattedMessage id={translationKey} values={{ unitPrice, quantity }} />
</span>
<span className={css.itemValue}>{total}</span>
</div>
) : null;
};
LineItemBasePriceMaybe.propTypes = {
transaction: propTypes.transaction.isRequired,
unitType: propTypes.bookingUnitType.isRequired,
intl: intlShape.isRequired,
};
export default LineItemBasePriceMaybe;

View file

@ -1,44 +1,63 @@
import React from 'react';
import { FormattedMessage, FormattedHTMLMessage, FormattedDate } from 'react-intl';
import { FormattedMessage, FormattedDate } from '../../util/reactIntl';
import moment from 'moment';
import { LINE_ITEM_NIGHT, LINE_ITEM_DAY, propTypes } from '../../util/types';
import { daysBetween, dateFromAPIToLocalNoon } from '../../util/dates';
import { LINE_ITEM_NIGHT, DATE_TYPE_DATE, propTypes } from '../../util/types';
import { dateFromAPIToLocalNoon } from '../../util/dates';
import css from './BookingBreakdown.css';
const BookingPeriod = props => {
const { isSingleDay, startDate, endDate } = props;
const { startDate, endDate, dateType } = props;
const timeFormatOptions =
dateType === DATE_TYPE_DATE
? {
weekday: 'long',
}
: {
weekday: 'short',
hour: 'numeric',
minute: 'numeric',
};
const dateFormatOptions = {
weekday: 'short',
month: 'short',
day: 'numeric',
};
if (isSingleDay) {
return <FormattedDate value={startDate} {...dateFormatOptions} />;
}
return (
<FormattedMessage
id="BookingBreakdown.bookingPeriod"
values={{
bookingStart: (
<span className={css.nowrap}>
<>
<div className={css.bookingPeriod}>
<div className={css.bookingPeriodSection}>
<div className={css.dayLabel}>
<FormattedMessage id="BookingBreakdown.bookingStart" />
</div>
<div className={css.dayInfo}>
<FormattedDate value={startDate} {...timeFormatOptions} />
</div>
<div className={css.itemLabel}>
<FormattedDate value={startDate} {...dateFormatOptions} />
</span>
),
bookingEnd: (
<span className={css.nowrap}>
</div>
</div>
<div className={css.bookingPeriodSectionRigth}>
<div className={css.dayLabel}>
<FormattedMessage id="BookingBreakdown.bookingEnd" />
</div>
<div className={css.dayInfo}>
<FormattedDate value={endDate} {...timeFormatOptions} />
</div>
<div className={css.itemLabel}>
<FormattedDate value={endDate} {...dateFormatOptions} />
</span>
),
}}
/>
</div>
</div>
</div>
</>
);
};
const LineItemBookingPeriod = props => {
const { transaction, booking, unitType } = props;
const { booking, unitType, dateType } = props;
// Attributes: displayStart and displayEnd can be used to differentiate shown time range
// from actual start and end times used for availability reservation. It can help in situations
@ -49,39 +68,22 @@ const LineItemBookingPeriod = props => {
const localEndDateRaw = dateFromAPIToLocalNoon(displayEnd || end);
const isNightly = unitType === LINE_ITEM_NIGHT;
const isDaily = unitType === LINE_ITEM_DAY;
const dayCount = daysBetween(localStartDate, localEndDateRaw);
const isSingleDay = !isNightly && dayCount === 1;
const endDay = isNightly ? localEndDateRaw : moment(localEndDateRaw).subtract(1, 'days');
const unitPurchase = transaction.attributes.lineItems.find(
item => item.code === unitType && !item.reversal
);
const useQuantityForDayCount = isNightly || isDaily;
const count = useQuantityForDayCount && unitPurchase ? unitPurchase.quantity.toFixed() : dayCount;
const unitCountMessage = (
<FormattedHTMLMessage
id={isNightly ? 'BookingBreakdown.nightCount' : 'BookingBreakdown.dayCount'}
values={{ count }}
/>
);
return (
<div className={css.lineItem}>
<span className={css.itemLabel}>
<BookingPeriod isSingleDay={isSingleDay} startDate={localStartDate} endDate={endDay} />
</span>
<span className={css.itemValue}>{unitCountMessage}</span>
</div>
<>
<div className={css.lineItem}>
<BookingPeriod startDate={localStartDate} endDate={endDay} dateType={dateType} />
</div>
<hr className={css.totalDivider} />
</>
);
};
LineItemBookingPeriod.defaultProps = { dateType: null };
LineItemBookingPeriod.propTypes = {
transaction: propTypes.transaction.isRequired,
booking: propTypes.booking.isRequired,
dateType: propTypes.dateType,
};
export default LineItemBookingPeriod;

View file

@ -1,6 +1,6 @@
import React from 'react';
import { bool } from 'prop-types';
import { FormattedMessage, intlShape } from 'react-intl';
import { FormattedMessage, intlShape } from '../../util/reactIntl';
import { formatMoney } from '../../util/currency';
import { types as sdkTypes } from '../../util/sdkLoader';
import { LINE_ITEM_CUSTOMER_COMMISSION, propTypes } from '../../util/types';

View file

@ -1,5 +1,5 @@
import React from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import { FormattedMessage, intlShape } from '../../util/reactIntl';
import { formatMoney } from '../../util/currency';
import { propTypes, LINE_ITEM_CUSTOMER_COMMISSION } from '../../util/types';

View file

@ -1,6 +1,6 @@
import React from 'react';
import { bool } from 'prop-types';
import { FormattedMessage, intlShape } from 'react-intl';
import { FormattedMessage, intlShape } from '../../util/reactIntl';
import { formatMoney } from '../../util/currency';
import { types as sdkTypes } from '../../util/sdkLoader';
import { LINE_ITEM_PROVIDER_COMMISSION, propTypes } from '../../util/types';

View file

@ -1,5 +1,5 @@
import React from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import { FormattedMessage, intlShape } from '../../util/reactIntl';
import { formatMoney } from '../../util/currency';
import { propTypes, LINE_ITEM_PROVIDER_COMMISSION } from '../../util/types';

View file

@ -1,5 +1,5 @@
import React from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import { FormattedMessage, intlShape } from '../../util/reactIntl';
import Decimal from 'decimal.js';
import { formatMoney } from '../../util/currency';
import { types as sdkTypes } from '../../util/sdkLoader';

View file

@ -1,6 +1,6 @@
import React from 'react';
import { string } from 'prop-types';
import { FormattedMessage, intlShape } from 'react-intl';
import { FormattedMessage, intlShape } from '../../util/reactIntl';
import Decimal from 'decimal.js';
import { formatMoney } from '../../util/currency';
import config from '../../config';
@ -81,12 +81,15 @@ const LineItemSubTotalMaybe = props => {
const formattedSubTotal = subTotalLineItems.length > 0 ? formatMoney(intl, subTotal) : null;
return formattedSubTotal && showSubTotal ? (
<div className={css.subTotalLineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.subTotal" />
</span>
<span className={css.itemValue}>{formattedSubTotal}</span>
</div>
<>
<hr className={css.totalDivider} />
<div className={css.subTotalLineItem}>
<span className={css.itemLabel}>
<FormattedMessage id="BookingBreakdown.subTotal" />
</span>
<span className={css.itemValue}>{formattedSubTotal}</span>
</div>
</>
) : null;
};

View file

@ -1,6 +1,6 @@
import React from 'react';
import { bool } from 'prop-types';
import { FormattedMessage, intlShape } from 'react-intl';
import { FormattedMessage, intlShape } from '../../util/reactIntl';
import { formatMoney } from '../../util/currency';
import { txIsCanceled, txIsDelivered, txIsDeclined } from '../../util/transaction';
import { propTypes } from '../../util/types';
@ -31,10 +31,13 @@ const LineItemUnitPrice = props => {
const formattedTotalPrice = formatMoney(intl, totalPrice);
return (
<div className={css.lineItem}>
<div className={css.totalLabel}>{totalLabel}</div>
<div className={css.totalPrice}>{formattedTotalPrice}</div>
</div>
<>
<hr className={css.totalDivider} />
<div className={css.lineItemTotal}>
<div className={css.totalLabel}>{totalLabel}</div>
<div className={css.totalPrice}>{formattedTotalPrice}</div>
</div>
</>
);
};

View file

@ -1,5 +1,5 @@
import React from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import { FormattedMessage, intlShape } from '../../util/reactIntl';
import { formatMoney } from '../../util/currency';
import { LINE_ITEM_NIGHT, LINE_ITEM_DAY, propTypes } from '../../util/types';

View file

@ -1,5 +1,5 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from '../../util/reactIntl';
import { LINE_ITEM_UNITS, propTypes } from '../../util/types';
import css from './BookingBreakdown.css';

View file

@ -10,7 +10,7 @@
* component for them that can be used in the `BookingBreakdown` component.
*/
import React from 'react';
import { intlShape } from 'react-intl';
import { intlShape } from '../../util/reactIntl';
import { formatMoney } from '../../util/currency';
import { humanizeLineItemCode } from '../../util/data';
import { LINE_ITEMS, propTypes } from '../../util/types';

View file

@ -4,43 +4,70 @@ exports[`BookingBreakdown customer transaction data matches snapshot 1`] = `
<div
className=""
>
<div>
<span>
<span>
BookingBreakdown.pricePerNight
</span>
</span>
<span>
10
</span>
</div>
<div>
<span>
<span>
BookingBreakdown.bookingPeriod
</span>
</span>
<span>
<span
dangerouslySetInnerHTML={
Object {
"__html": "BookingBreakdown.nightCount",
}
}
/>
</span>
</div>
<hr />
<div>
Array [
<div>
<div>
<div>
<div>
<span>
BookingBreakdown.bookingStart
</span>
</div>
<div>
<span>
Sat 12:00 PM
</span>
</div>
<div>
<span>
Jun 10
</span>
</div>
</div>
<div>
<div>
<span>
BookingBreakdown.bookingEnd
</span>
</div>
<div>
<span>
Sat 12:00 PM
</span>
</div>
<div>
<span>
Jun 10
</span>
</div>
</div>
</div>
</div>,
<hr />,
]
<div>
<span>
<span>
BookingBreakdown.total
BookingBreakdown.baseUnitNight
</span>
</div>
<div>
</span>
<span>
20
</div>
</span>
</div>
Array [
<hr />,
<div>
<div>
<span>
BookingBreakdown.total
</span>
</div>
<div>
20
</div>
</div>,
]
</div>
`;
@ -48,43 +75,70 @@ exports[`BookingBreakdown pretransaction data matches snapshot 1`] = `
<div
className=""
>
<div>
<span>
<span>
BookingBreakdown.pricePerNight
</span>
</span>
<span>
10
</span>
</div>
<div>
<span>
<span>
BookingBreakdown.bookingPeriod
</span>
</span>
<span>
<span
dangerouslySetInnerHTML={
Object {
"__html": "BookingBreakdown.nightCount",
}
}
/>
</span>
</div>
<hr />
<div>
Array [
<div>
<div>
<div>
<div>
<span>
BookingBreakdown.bookingStart
</span>
</div>
<div>
<span>
Sat 12:00 PM
</span>
</div>
<div>
<span>
Jun 10
</span>
</div>
</div>
<div>
<div>
<span>
BookingBreakdown.bookingEnd
</span>
</div>
<div>
<span>
Sat 12:00 PM
</span>
</div>
<div>
<span>
Jun 10
</span>
</div>
</div>
</div>
</div>,
<hr />,
]
<div>
<span>
<span>
BookingBreakdown.total
BookingBreakdown.baseUnitNight
</span>
</div>
<div>
</span>
<span>
20
</div>
</span>
</div>
Array [
<hr />,
<div>
<div>
<span>
BookingBreakdown.total
</span>
</div>
<div>
20
</div>
</div>,
]
</div>
`;
@ -92,42 +146,70 @@ exports[`BookingBreakdown provider canceled transaction data matches snapshot 1`
<div
className=""
>
Array [
<div>
<div>
<div>
<div>
<span>
BookingBreakdown.bookingStart
</span>
</div>
<div>
<span>
Sat 12:00 PM
</span>
</div>
<div>
<span>
Jun 10
</span>
</div>
</div>
<div>
<div>
<span>
BookingBreakdown.bookingEnd
</span>
</div>
<div>
<span>
Sat 12:00 PM
</span>
</div>
<div>
<span>
Jun 10
</span>
</div>
</div>
</div>
</div>,
<hr />,
]
<div>
<span>
<span>
BookingBreakdown.pricePerNight
</span>
</span>
<span>
10
</span>
</div>
<div>
<span>
<span>
BookingBreakdown.bookingPeriod
</span>
</span>
<span>
<span
dangerouslySetInnerHTML={
Object {
"__html": "BookingBreakdown.nightCount",
}
}
/>
</span>
</div>
<div>
<span>
<span>
BookingBreakdown.subTotal
BookingBreakdown.baseUnitNight
</span>
</span>
<span>
20
</span>
</div>
Array [
<hr />,
<div>
<span>
<span>
BookingBreakdown.subTotal
</span>
</span>
<span>
20
</span>
</div>,
]
<div>
<span>
<span>
@ -158,17 +240,19 @@ exports[`BookingBreakdown provider canceled transaction data matches snapshot 1`
2
</span>
</div>
<hr />
<div>
Array [
<hr />,
<div>
<span>
BookingBreakdown.providerTotalCanceled
</span>
</div>
<div>
0
</div>
</div>
<div>
<span>
BookingBreakdown.providerTotalCanceled
</span>
</div>
<div>
0
</div>
</div>,
]
<span>
<span>
BookingBreakdown.commissionFeeNote
@ -181,42 +265,70 @@ exports[`BookingBreakdown provider transaction data matches snapshot 1`] = `
<div
className=""
>
Array [
<div>
<div>
<div>
<div>
<span>
BookingBreakdown.bookingStart
</span>
</div>
<div>
<span>
Sat 12:00 PM
</span>
</div>
<div>
<span>
Jun 10
</span>
</div>
</div>
<div>
<div>
<span>
BookingBreakdown.bookingEnd
</span>
</div>
<div>
<span>
Sat 12:00 PM
</span>
</div>
<div>
<span>
Jun 10
</span>
</div>
</div>
</div>
</div>,
<hr />,
]
<div>
<span>
<span>
BookingBreakdown.pricePerNight
</span>
</span>
<span>
10
</span>
</div>
<div>
<span>
<span>
BookingBreakdown.bookingPeriod
</span>
</span>
<span>
<span
dangerouslySetInnerHTML={
Object {
"__html": "BookingBreakdown.nightCount",
}
}
/>
</span>
</div>
<div>
<span>
<span>
BookingBreakdown.subTotal
BookingBreakdown.baseUnitNight
</span>
</span>
<span>
20
</span>
</div>
Array [
<hr />,
<div>
<span>
<span>
BookingBreakdown.subTotal
</span>
</span>
<span>
20
</span>
</div>,
]
<div>
<span>
<span>
@ -227,17 +339,19 @@ exports[`BookingBreakdown provider transaction data matches snapshot 1`] = `
-2
</span>
</div>
<hr />
<div>
Array [
<hr />,
<div>
<span>
BookingBreakdown.providerTotalDefault
</span>
</div>
<div>
18
</div>
</div>
<div>
<span>
BookingBreakdown.providerTotalDefault
</span>
</div>
<div>
18
</div>
</div>,
]
<span>
<span>
BookingBreakdown.commissionFeeNote

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { bool, func, number, object, string } from 'prop-types';
import { injectIntl, intlShape } from 'react-intl';
import { injectIntl, intlShape } from '../../util/reactIntl';
import { FieldDateRangeController, FilterPopup, FilterPlain } from '../../components';
import css from './BookingDateRangeFilter.css';

View file

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BookingDateRangeFilter matches plain snapshot 1`] = `
<InjectIntl(FilterPlainComponent)
<injectIntl(FilterPlainComponent)
className={null}
contentPlacementOffset={-14}
id="BookingDateRangeFilter.plain"
@ -23,11 +23,11 @@ exports[`BookingDateRangeFilter matches plain snapshot 1`] = `
name="dates"
rootClassName={null}
/>
</InjectIntl(FilterPlainComponent)>
</injectIntl(FilterPlainComponent)>
`;
exports[`BookingDateRangeFilter matches popup snapshot 1`] = `
<InjectIntl(FilterPopup)
<injectIntl(FilterPopup)
className={null}
contentPlacementOffset={-14}
id="BookingDateRangeFilter.popup"
@ -50,5 +50,5 @@ exports[`BookingDateRangeFilter matches popup snapshot 1`] = `
name="dates"
rootClassName={null}
/>
</InjectIntl(FilterPopup)>
</injectIntl(FilterPopup)>
`;

View file

@ -1,9 +1,8 @@
import React from 'react';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { intlShape, injectIntl } from 'react-intl';
import { intlShape, injectIntl, FormattedMessage } from '../../util/reactIntl';
import { arrayOf, bool, func, node, oneOfType, shape, string } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import omit from 'lodash/omit';
import { propTypes, LISTING_STATE_CLOSED, LINE_ITEM_NIGHT, LINE_ITEM_DAY } from '../../util/types';
@ -118,6 +117,7 @@ const BookingPanel = props => {
{showBookingDatesForm ? (
<BookingDatesForm
className={css.bookingForm}
formId="BookingPanel"
submitButtonWrapperClassName={css.bookingDatesSubmitButtonWrapper}
unitType={unitType}
onSubmit={onSubmit}
@ -176,7 +176,7 @@ BookingPanel.propTypes = {
onSubmit: func.isRequired,
title: oneOfType([node, string]).isRequired,
subTitle: oneOfType([node, string]),
authorDisplayName: string.isRequired,
authorDisplayName: oneOfType([node, string]).isRequired,
onManageDisableScrolling: func.isRequired,
timeSlots: arrayOf(propTypes.timeSlot),
fetchTimeSlotsError: propTypes.error,

View file

@ -0,0 +1,14 @@
@import '../../marketplace.css';
.root {
}
.bookingInfo {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.dateSection {
margin-right: 5px;
}

View file

@ -0,0 +1,141 @@
import BookingTimeInfo from './BookingTimeInfo';
import {
fakeIntl,
createBooking,
createTransaction,
createUser,
createListing,
} from '../../util/test-data';
import { LINE_ITEM_DAY, LINE_ITEM_NIGHT, LINE_ITEM_UNITS } from '../../util/types';
export const DateAndTimeSingleDay = {
component: BookingTimeInfo,
props: {
isOrder: true,
intl: fakeIntl,
tx: createTransaction({
customer: createUser('user1'),
provider: createUser('user2'),
listing: createListing('Listing'),
booking: createBooking('example-booking', {
displayStart: new Date(Date.UTC(2019, 8, 30, 3, 0)),
displayEnd: new Date(Date.UTC(2019, 8, 30, 4, 0)),
start: new Date(Date.UTC(2019, 8, 30, 3, 0)),
end: new Date(Date.UTC(2019, 8, 30, 4, 0)),
}),
}),
unitType: LINE_ITEM_UNITS,
dateType: 'datetime',
},
group: 'inbox',
};
export const DateAndTimeMultipleDays = {
component: BookingTimeInfo,
props: {
isOrder: true,
intl: fakeIntl,
tx: createTransaction({
customer: createUser('user1'),
provider: createUser('user2'),
listing: createListing('Listing'),
booking: createBooking('example-booking', {
displayStart: new Date(Date.UTC(2019, 8, 28, 3, 0)),
displayEnd: new Date(Date.UTC(2019, 8, 30, 5, 0)),
start: new Date(Date.UTC(2019, 8, 28, 3, 0)),
end: new Date(Date.UTC(2019, 8, 30, 5, 0)),
}),
}),
unitType: LINE_ITEM_UNITS,
dateType: 'datetime',
},
group: 'inbox',
};
export const OnlyDateSingleDay = {
component: BookingTimeInfo,
props: {
isOrder: true,
intl: fakeIntl,
tx: createTransaction({
customer: createUser('user1'),
provider: createUser('user2'),
listing: createListing('Listing'),
booking: createBooking('example-booking', {
displayStart: new Date(Date.UTC(2019, 8, 29, 3, 0)),
displayEnd: new Date(Date.UTC(2019, 8, 30, 4, 0)),
start: new Date(Date.UTC(2019, 8, 29, 3, 0)),
end: new Date(Date.UTC(2019, 8, 30, 4, 0)),
}),
}),
unitType: LINE_ITEM_DAY,
dateType: 'date',
},
group: 'inbox',
};
export const OnlyDateMultipleDays = {
component: BookingTimeInfo,
props: {
isOrder: true,
intl: fakeIntl,
tx: createTransaction({
customer: createUser('user1'),
provider: createUser('user2'),
listing: createListing('Listing'),
booking: createBooking('example-booking', {
displayStart: new Date(Date.UTC(2019, 8, 28, 3, 0)),
displayEnd: new Date(Date.UTC(2019, 8, 30, 5, 0)),
start: new Date(Date.UTC(2019, 8, 28, 3, 0)),
end: new Date(Date.UTC(2019, 8, 30, 5, 0)),
}),
}),
unitType: LINE_ITEM_DAY,
dateType: 'date',
},
group: 'inbox',
};
export const OnlyDateSingleNight = {
component: BookingTimeInfo,
props: {
isOrder: true,
intl: fakeIntl,
tx: createTransaction({
customer: createUser('user1'),
provider: createUser('user2'),
listing: createListing('Listing'),
booking: createBooking('example-booking', {
displayStart: new Date(Date.UTC(2019, 8, 29, 3, 0)),
displayEnd: new Date(Date.UTC(2019, 8, 30, 4, 0)),
start: new Date(Date.UTC(2019, 8, 29, 3, 0)),
end: new Date(Date.UTC(2019, 8, 30, 4, 0)),
}),
}),
unitType: LINE_ITEM_NIGHT,
dateType: 'date',
},
group: 'inbox',
};
export const OnlyDateMultipleNights = {
component: BookingTimeInfo,
props: {
isOrder: true,
intl: fakeIntl,
tx: createTransaction({
customer: createUser('user1'),
provider: createUser('user2'),
listing: createListing('Listing'),
booking: createBooking('example-booking', {
displayStart: new Date(Date.UTC(2019, 8, 28, 3, 0)),
displayEnd: new Date(Date.UTC(2019, 8, 30, 5, 0)),
start: new Date(Date.UTC(2019, 8, 28, 3, 0)),
end: new Date(Date.UTC(2019, 8, 30, 5, 0)),
}),
}),
unitType: LINE_ITEM_NIGHT,
dateType: 'date',
},
group: 'inbox',
};

View file

@ -0,0 +1,98 @@
import React from 'react';
import moment from 'moment';
import { bool } from 'prop-types';
import classNames from 'classnames';
import { txIsEnquired } from '../../util/transaction';
import { dateFromAPIToLocalNoon, daysBetween, formatDateToText } from '../../util/dates';
import { injectIntl, intlShape } from '../../util/reactIntl';
import {
LINE_ITEM_DAY,
LINE_ITEM_NIGHT,
LINE_ITEM_UNITS,
DATE_TYPE_DATE,
DATE_TYPE_DATETIME,
propTypes,
} from '../../util/types';
import css from './BookingTimeInfo.css';
const bookingData = (unitType, tx, isOrder, intl) => {
// Attributes: displayStart and displayEnd can be used to differentiate shown time range
// from actual start and end times used for availability reservation. It can help in situations
// where there are preparation time needed between bookings.
// Read more: https://www.sharetribe.com/api-reference/#bookings
const { start, end, displayStart, displayEnd } = tx.booking.attributes;
const startDate = dateFromAPIToLocalNoon(displayStart || start);
const endDateRaw = dateFromAPIToLocalNoon(displayEnd || end);
const isDaily = unitType === LINE_ITEM_DAY;
const isNightly = unitType === LINE_ITEM_NIGHT;
const isUnits = unitType === LINE_ITEM_UNITS;
const isSingleDay = !isNightly && daysBetween(startDate, endDateRaw) <= 1;
const bookingStart = formatDateToText(intl, startDate);
// Shift the exclusive API end date with daily bookings
const endDate =
isDaily || isUnits
? moment(endDateRaw)
.subtract(1, 'days')
.toDate()
: endDateRaw;
const bookingEnd = formatDateToText(intl, endDate);
return { bookingStart, bookingEnd, isSingleDay };
};
const BookingTimeInfoComponent = props => {
const { bookingClassName, isOrder, intl, tx, unitType, dateType } = props;
const isEnquiry = txIsEnquired(tx);
if (isEnquiry) {
return null;
}
const bookingTimes = bookingData(unitType, tx, isOrder, intl);
const { bookingStart, bookingEnd, isSingleDay } = bookingTimes;
if (isSingleDay && dateType === DATE_TYPE_DATE) {
return (
<div className={classNames(css.bookingInfo, bookingClassName)}>
<span className={css.dateSection}>{`${bookingStart.date}`}</span>
</div>
);
} else if (dateType === DATE_TYPE_DATE) {
return (
<div className={classNames(css.bookingInfo, bookingClassName)}>
<span className={css.dateSection}>{`${bookingStart.date} -`}</span>
<span className={css.dateSection}>{`${bookingEnd.date}`}</span>
</div>
);
} else if (isSingleDay && dateType === DATE_TYPE_DATETIME) {
return (
<div className={classNames(css.bookingInfo, bookingClassName)}>
<span className={css.dateSection}>
{`${bookingStart.date}, ${bookingStart.time} - ${bookingEnd.time}`}
</span>
</div>
);
} else {
return (
<div className={classNames(css.bookingInfo, bookingClassName)}>
<span className={css.dateSection}>{`${bookingStart.dateAndTime} - `}</span>
<span className={css.dateSection}>{`${bookingEnd.dateAndTime}`}</span>
</div>
);
}
};
BookingTimeInfoComponent.defaultProps = { dateType: null };
BookingTimeInfoComponent.propTypes = {
intl: intlShape.isRequired,
isOrder: bool.isRequired,
tx: propTypes.transaction.isRequired,
unitType: propTypes.bookingUnitType.isRequired,
dateType: propTypes.dateType,
};
const BookingTimeInfo = injectIntl(BookingTimeInfoComponent);
export default BookingTimeInfo;

View file

@ -1,4 +1,3 @@
/* eslint-disable jsx-a11y/href-no-hash */
import React, { Component } from 'react';
import { IconCheckmark } from '../../components';
import Button, { PrimaryButton, SecondaryButton, InlineTextButton } from './Button';

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from '../../util/reactIntl';
import { ExternalLink } from '../../components';
import classNames from 'classnames';

View file

@ -1,7 +1,7 @@
import React from 'react';
import { bool, func, object, shape, string } from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from '../../util/reactIntl';
import { ensureOwnListing } from '../../util/data';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { ListingLink } from '../../components';

View file

@ -1,7 +1,7 @@
import React from 'react';
import { bool, func, object, string } from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from '../../util/reactIntl';
import { ensureOwnListing } from '../../util/data';
import { ListingLink } from '../../components';
import { LISTING_STATE_DRAFT } from '../../util/types';

View file

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from '../../util/reactIntl';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { ensureListing } from '../../util/data';

View file

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from '../../util/reactIntl';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { ensureOwnListing } from '../../util/data';
import { ListingLink } from '../../components';

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { array, bool, func, object, string } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from '../../util/reactIntl';
import classNames from 'classnames';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { EditListingPhotosForm } from '../../forms';

View file

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from '../../util/reactIntl';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { ensureOwnListing } from '../../util/data';
import { ListingLink } from '../../components';

View file

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from '../../util/reactIntl';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { ListingLink } from '../../components';
import { EditListingPricingForm } from '../../forms';

View file

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { array, bool, func, number, object, oneOf, shape, string } from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl';
import classNames from 'classnames';
import config from '../../config';
import { withViewport } from '../../util/contextHelpers';
@ -303,6 +303,7 @@ EditListingWizard.defaultProps = {
className: null,
rootClassName: null,
listing: null,
updateInProgress: false,
};
EditListingWizard.propTypes = {
@ -340,6 +341,7 @@ EditListingWizard.propTypes = {
onPayoutDetailsFormChange: func.isRequired,
onPayoutDetailsSubmit: func.isRequired,
onManageDisableScrolling: func.isRequired,
updateInProgress: bool,
// from withViewport
viewport: shape({

View file

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'react-intl';
import { intlShape } from '../../util/reactIntl';
import routeConfiguration from '../../routeConfiguration';
import {
LISTING_PAGE_PARAM_TYPE_DRAFT,

View file

@ -4,6 +4,8 @@ import { Form as FinalForm, FormSpy } from 'react-final-form';
import * as validators from '../../util/validators';
import FieldBirthdayInput from './FieldBirthdayInput';
const identity = v => v;
const FormComponent = props => (
<FinalForm
{...props}
@ -25,8 +27,8 @@ const FormComponent = props => (
id={`birthday`}
name="birthday"
label="Date of birth"
format={null}
valueFromForm={values.birthDate}
format={identity}
valueFromForm={values.birthday}
validate={validators.composeValidators(required, minAgeRequired)}
/>
</form>
@ -41,7 +43,24 @@ export const Empty = {
onChange: formState => {
const birthday = formState.values.birthday;
if (birthday) {
console.log('birthday changed to:', birthday.toUTCString());
console.log('birthday changed to:', birthday);
}
},
onSubmit: values => {
console.log('BirthdayInput.Form submitted values:', values);
},
},
group: 'custom inputs',
};
export const WithInitialValue = {
component: FormComponent,
props: {
initialValues: { birthday: new Date('1982-03-01') },
onChange: formState => {
const birthday = formState.values.birthday;
if (birthday && formState.dirty) {
console.log('birthday changed to:', birthday);
}
},
onSubmit: values => {

View file

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { func, instanceOf, object, node, string, bool } from 'prop-types';
import { Field } from 'react-final-form';
import { injectIntl, intlShape } from 'react-intl';
import { injectIntl, intlShape } from '../../util/reactIntl';
import classNames from 'classnames';
import range from 'lodash/range';
import { ValidationError } from '../../components';
@ -83,15 +83,15 @@ class BirthdayInputComponent extends Component {
this.handleSelectBlur = this.handleSelectBlur.bind(this);
this.handleSelectChange = this.handleSelectChange.bind(this);
}
componentWillMount() {
componentDidMount() {
const value = this.props.valueFromForm;
if (value instanceof Date) {
this.setState({ selected: selectedFromDate(value) });
}
}
componentWillReceiveProps(newProps) {
const oldValue = this.props.valueFromForm;
const newValue = newProps.valueFromForm;
componentDidUpdate(prevProps) {
const oldValue = prevProps.valueFromForm;
const newValue = this.props.valueFromForm;
const valueChanged = oldValue !== newValue;
if (valueChanged && newValue instanceof Date) {
this.setState({ selected: selectedFromDate(newValue) });
@ -259,6 +259,7 @@ const FieldBirthdayInputComponent = props => {
disabled,
input,
meta,
valueFromForm,
} = props;
const { valid, invalid, touched, error } = meta;
@ -287,6 +288,7 @@ const FieldBirthdayInputComponent = props => {
monthLabel,
yearLabel,
disabled,
valueFromForm,
...input,
};
const classes = classNames(rootClassName || css.fieldRoot, className);

View file

@ -9,7 +9,7 @@ const formName = 'Styleguide.FieldBoolean.Form';
const FormComponent = props => (
<FinalForm
{...props}
form={formName}
formId={formName}
render={fieldRenderProps => {
const { form, handleSubmit, onChange, invalid, pristine, submitting } = props;
const required = validators.requiredBoolean('This field is required');

View file

@ -1,5 +1,5 @@
import React from 'react';
import { injectIntl, intlShape } from 'react-intl';
import { injectIntl, intlShape } from '../../util/reactIntl';
import { FieldSelect } from '../../components';
const FieldBoolean = props => {

View file

@ -17,6 +17,13 @@
stroke: var(--marketplaceColor);
}
/* successColor version */
&:hover + label .boxSuccess,
&:focus + label .boxSuccess,
&:checked + label .boxSuccess {
stroke: var(--successColor);
}
/* Display the "check" when checked */
&:checked + label .checked {
display: inline;
@ -24,6 +31,13 @@
stroke-width: 1px;
}
/* Display the "check" when checked */
&:checked + label .checkedSuccess {
display: inline;
stroke: var(--successColor);
stroke-width: 1px;
}
/* Hightlight the text on checked, hover and focus */
&:focus + label .text,
&:hover + label .text,
@ -54,12 +68,19 @@
display: none;
fill: var(--marketplaceColor);
}
.checkedSuccess {
display: none;
fill: var(--successColor);
}
.boxSuccess,
.box {
stroke: var(--matterColorAnti);
}
.text {
}
.textRoot {
@apply --marketplaceListingAttributeFontStyles;
color: var(--matterColor);
margin-top: -1px;

View file

@ -8,7 +8,7 @@ const formName = 'Styleguide.FieldCheckbox.Form';
const FormComponent = props => (
<FinalForm
{...props}
form={formName}
formId={formName}
render={fieldRenderProps => {
const { form, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps;

View file

@ -6,16 +6,17 @@ import { Field } from 'react-final-form';
import css from './FieldCheckbox.css';
const IconCheckbox = props => {
const { className, checkedClassName, boxClassName } = props;
return (
<svg className={props.className} width="14" height="14" xmlns="http://www.w3.org/2000/svg">
<svg className={className} width="14" height="14" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<g transform="translate(2 2)">
<path
className={css.checked}
className={checkedClassName || css.checked}
d="M9.9992985 1.5048549l-.0194517 6.9993137C9.977549 9.3309651 9.3066522 10 8.4798526 10H1.5001008c-.8284271 0-1.5-.6715729-1.5-1.5l-.000121-7c0-.8284271.6715728-1.5 1.5-1.5h.000121l6.9993246.0006862c.8284272.000067 1.4999458.671694 1.499879 1.5001211a1.5002208 1.5002208 0 0 1-.0000059.0040476z"
/>
<path
className={css.box}
className={boxClassName || css.box}
strokeWidth="2"
d="M10.9992947 1.507634l-.0194518 6.9993137C10.9760133 9.8849417 9.8578519 11 8.4798526 11H1.5001008c-1.3807119 0-2.5-1.1192881-2.5-2.4999827L-1.0000202 1.5c0-1.3807119 1.119288-2.5 2.500098-2.5l6.9994284.0006862c1.3807118.0001115 2.4999096 1.11949 2.4997981 2.5002019-.0000018.003373-.0000018.003373-.0000096.0067458z"
/>
@ -29,12 +30,21 @@ const IconCheckbox = props => {
);
};
IconCheckbox.defaultProps = { className: null };
IconCheckbox.defaultProps = { className: null, checkedClassName: null, boxClassName: null };
IconCheckbox.propTypes = { className: string };
IconCheckbox.propTypes = { className: string, checkedClassName: string, boxClassName: string };
const FieldCheckboxComponent = props => {
const { rootClassName, className, svgClassName, id, label, ...rest } = props;
const {
rootClassName,
className,
svgClassName,
textClassName,
id,
label,
useSuccessColor,
...rest
} = props;
const classes = classNames(rootClassName || css.root, className);
const checkboxProps = {
@ -45,14 +55,21 @@ const FieldCheckboxComponent = props => {
...rest,
};
const successColorVariantMaybe = useSuccessColor
? {
checkedClassName: css.checkedSuccess,
boxClassName: css.boxSuccess,
}
: {};
return (
<span className={classes}>
<Field {...checkboxProps} />
<label htmlFor={id} className={css.label}>
<span className={css.checkboxWrapper}>
<IconCheckbox className={svgClassName} />
<IconCheckbox className={svgClassName} {...successColorVariantMaybe} />
</span>
<span className={css.text}>{label}</span>
<span className={classNames(css.text, textClassName || css.textRoot)}>{label}</span>
</label>
</span>
);
@ -62,6 +79,7 @@ FieldCheckboxComponent.defaultProps = {
className: null,
rootClassName: null,
svgClassName: null,
textClassName: null,
label: null,
};
@ -69,6 +87,7 @@ FieldCheckboxComponent.propTypes = {
className: string,
rootClassName: string,
svgClassName: string,
textClassName: string,
// Id is needed to connect the label with input.
id: string.isRequired,

View file

@ -1,10 +1,8 @@
/* eslint-disable no-console */
import React from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import en from 'react-intl/locale-data/en';
import fi from 'react-intl/locale-data/fi';
import { IntlProvider } from '../../util/reactIntl';
import { currencyConfig } from '../../util/test-data';
import * as validators from '../../util/validators';
import FieldCurrencyInput, { CurrencyInput } from './FieldCurrencyInput';
@ -24,13 +22,8 @@ const onChange = price => console.log('CurrencyInput - value:', price);
// Different locales need to be initialized before their currency formatting is in use
const CurrencyInputWithIntl = ({ locale, ...rest }) => {
if (locale === 'en') {
addLocaleData([...en]);
} else {
addLocaleData([...fi]);
}
return (
<IntlProvider locale={locale}>
<IntlProvider locale={locale} textComponent="span">
<CurrencyInput {...rest} input={{ onChange }} />
</IntlProvider>
);

View file

@ -5,7 +5,7 @@
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { intlShape, injectIntl } from 'react-intl';
import { intlShape, injectIntl } from '../../util/reactIntl';
import { Field } from 'react-final-form';
import classNames from 'classnames';
import Decimal from 'decimal.js';

View file

@ -12,7 +12,7 @@ import {
isInclusivelyBeforeDay,
isSameDay,
} from 'react-dates';
import { intlShape, injectIntl } from 'react-intl';
import { intlShape, injectIntl } from '../../util/reactIntl';
import classNames from 'classnames';
import moment from 'moment';
import config from '../../config';

View file

@ -7,6 +7,8 @@ import { required, bookingDateRequired, composeValidators } from '../../util/val
import { createTimeSlots } from '../../util/test-data';
import FieldDateInput from './FieldDateInput';
const identity = v => v;
const createAvailableTimeSlots = (dayCount, availableDayCount) => {
const slots = createTimeSlots(new Date(), dayCount);
const availableSlotIndices = Array.from({ length: availableDayCount }, () =>
@ -64,7 +66,7 @@ export const Empty = {
id: `EmptyDateInputForm.bookingDate`,
label: 'Date',
placeholderText: moment().format('ddd, MMMM D'),
format: null,
format: identity,
validate: composeValidators(required('Required'), bookingDateRequired('Date is not valid')),
onBlur: () => console.log('onBlur called from DateInput props.'),
onFocus: () => console.log('onFocus called from DateInput props.'),
@ -91,7 +93,7 @@ export const WithAvailableTimeSlots = {
id: `AvailableTimeSlotsDateInputForm.bookingDate`,
label: 'Date',
placeholderText: moment().format('ddd, MMMM D'),
format: null,
format: identity,
timeSlots: createAvailableTimeSlots(90, 60),
validate: composeValidators(required('Required'), bookingDateRequired('Date is not valid')),
onBlur: () => console.log('onBlur called from DateInput props.'),

View file

@ -47,7 +47,7 @@ class FieldDateInputComponent extends Component {
[css.pickerError]: hasError,
});
const { onBlur, onFocus, ...restOfInput } = input;
const { onBlur, onFocus, type, ...restOfInput } = input;
const inputProps = {
onBlur: input.onBlur,
onFocus: input.onFocus,

View file

@ -10,16 +10,19 @@ import { DateInput } from './FieldDateInput';
const noop = () => null;
describe('DateInput', () => {
it('matches snapshot', () => {
const props = {
name: 'bookingDate',
onBlur: noop,
onChange: noop,
onFocus: noop,
id: 'bookingDate',
placeholderText: 'today',
};
const tree = renderDeep(<DateInput {...props} />);
expect(tree).toMatchSnapshot();
it('TODO, wait react-dates to work with React 16.9 without warnings', () => {
expect('todo').toEqual('todo');
});
// it('matches snapshot', () => {
// const props = {
// name: 'bookingDate',
// onBlur: noop,
// onChange: noop,
// onFocus: noop,
// id: 'bookingDate',
// placeholderText: 'today',
// };
// const tree = renderDeep(<DateInput {...props} />);
// expect(tree).toMatchSnapshot();
// });
});

View file

@ -1,45 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DateInput matches snapshot 1`] = `
<div
className=""
>
<div
className="SingleDatePicker SingleDatePicker_1"
>
<div>
<div
className="SingleDatePickerInput SingleDatePickerInput_1"
>
<div
className="DateInput DateInput_1"
>
<input
aria-describedby="DateInput__screen-reader-message-bookingDate"
aria-label="today"
autoComplete="off"
className="DateInput_input DateInput_input_1"
disabled={false}
id="bookingDate"
name="bookingDate"
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="today"
readOnly={false}
required={false}
type="text"
value=""
/>
<p
className="DateInput_screenReaderMessage DateInput_screenReaderMessage_1"
id="DateInput__screen-reader-message-bookingDate"
>
FieldDateInput.screenReaderInputMessage
</p>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -5,7 +5,8 @@ import DateRangeController from './DateRangeController';
const component = props => {
const { input, controllerRef, ...rest } = props;
return <DateRangeController ref={controllerRef} {...input} {...rest} />;
const { type, ...restOfInput } = input;
return <DateRangeController ref={controllerRef} {...restOfInput} {...rest} />;
};
const FieldDateRangeController = props => {

View file

@ -7,7 +7,7 @@
import React, { Component } from 'react';
import { bool, func, instanceOf, oneOf, shape, string, arrayOf } from 'prop-types';
import { DateRangePicker, isInclusivelyAfterDay, isInclusivelyBeforeDay } from 'react-dates';
import { intlShape, injectIntl } from 'react-intl';
import { intlShape, injectIntl } from '../../util/reactIntl';
import classNames from 'classnames';
import moment from 'moment';
import { START_DATE, END_DATE } from '../../util/dates';
@ -138,12 +138,12 @@ class DateRangeInputComponent extends Component {
this.onFocusChange = this.onFocusChange.bind(this);
}
componentWillReceiveProps(nextProps) {
componentDidUpdate(prevProps) {
// Update focusedInput in case a new value for it is
// passed in the props. This may occur if the focus
// is manually set to the date picker.
if (nextProps.focusedInput && nextProps.focusedInput !== this.props.focusedInput) {
this.setState({ focusedInput: nextProps.focusedInput });
if (this.props.focusedInput && this.props.focusedInput !== prevProps.focusedInput) {
this.setState({ focusedInput: this.props.focusedInput });
}
}

View file

@ -8,6 +8,8 @@ import { LINE_ITEM_NIGHT, LINE_ITEM_DAY } from '../../util/types';
import { createTimeSlots } from '../../util/test-data';
import FieldDateRangeInput from './FieldDateRangeInput';
const identity = v => v;
const createAvailableTimeSlots = (dayCount, availableDayCount) => {
const slots = createTimeSlots(new Date(), dayCount);
@ -70,7 +72,7 @@ export const Empty = {
endDatePlaceholderText: moment()
.add(1, 'days')
.format('ddd, MMMM D'),
format: null,
format: identity,
validate: composeValidators(
required('Required'),
bookingDatesRequired('Start date is not valid', 'End date is not valid')
@ -101,12 +103,12 @@ export const WithAvailableTimeSlotsNighlyBooking = {
startDateId: 'WithAvailableTimeSlotsDateRangeNightly.bookingStartDate',
startDateLabel: 'Start date',
startDatePlaceholderText: moment().format('ddd, MMMM D'),
endDateId: 'WithAvailableTimeSlotsDateRangeInputForm.bookingEndDate',
endDateId: 'WithAvailableTimeSlotsDateRangeNightly.bookingEndDate',
endDateLabel: 'End date',
endDatePlaceholderText: moment()
.add(1, 'days')
.format('ddd, MMMM D'),
format: null,
format: identity,
timeSlots: createAvailableTimeSlots(90, 60),
validate: composeValidators(
required('Required'),
@ -137,12 +139,12 @@ export const WithAvailableTimeSlotsDailyBooking = {
startDateId: 'WithAvailableTimeSlotsDateRangeDaily.bookingStartDate',
startDateLabel: 'Start date',
startDatePlaceholderText: moment().format('ddd, MMMM D'),
endDateId: 'WithAvailableTimeSlotsDateRangeInputForm.bookingEndDate',
endDateId: 'WithAvailableTimeSlotsDateRangeDaily.bookingEndDate',
endDateLabel: 'End date',
endDatePlaceholderText: moment()
.add(1, 'days')
.format('ddd, MMMM D'),
format: null,
format: identity,
timeSlots: createAvailableTimeSlots(90, 60),
validate: composeValidators(
required('Required'),

View file

@ -26,12 +26,12 @@ class FieldDateRangeInputComponent extends Component {
this.handleFocus = this.handleFocus.bind(this);
}
componentWillReceiveProps(nextProps) {
componentDidUpdate(prevProps) {
// Update focusedInput in case a new value for it is
// passed in the props. This may occur if the focus
// is manually set to the date picker.
if (nextProps.focusedInput && nextProps.focusedInput !== this.props.focusedInput) {
this.setState({ focusedInput: nextProps.focusedInput });
if (this.props.focusedInput && this.props.focusedInput !== prevProps.focusedInput) {
this.setState({ focusedInput: this.props.focusedInput });
}
}
@ -117,7 +117,7 @@ class FieldDateRangeInputComponent extends Component {
) : null;
// eslint-disable-next-line no-unused-vars
const { onBlur, onFocus, ...restOfInput } = input;
const { onBlur, onFocus, type, ...restOfInput } = input;
const inputProps = {
unitType,
onBlur: this.handleBlur,

View file

@ -11,19 +11,23 @@ import { DateRangeInput } from './FieldDateRangeInput';
const noop = () => null;
describe('DateRangeInput', () => {
it('matches snapshot', () => {
const props = {
unitType: LINE_ITEM_NIGHT,
name: 'bookingDates',
onBlur: noop,
onChange: noop,
onFocus: noop,
startDateId: 'bookingStartDate',
startDatePlaceholderText: 'today',
endDateId: 'bookingEndDate',
endDatePlaceholderText: 'tomorrow',
};
const tree = renderDeep(<DateRangeInput {...props} />);
expect(tree).toMatchSnapshot();
it('TODO, wait react-dates to work with React 16.9 without warnings', () => {
expect('todo').toEqual('todo');
});
// it('matches snapshot', () => {
// const props = {
// unitType: LINE_ITEM_NIGHT,
// name: 'bookingDates',
// onBlur: noop,
// onChange: noop,
// onFocus: noop,
// startDateId: 'bookingStartDate',
// startDatePlaceholderText: 'today',
// endDateId: 'bookingEndDate',
// endDatePlaceholderText: 'tomorrow',
// };
// const tree = renderDeep(<DateRangeInput {...props} />);
// expect(tree).toMatchSnapshot();
// });
});

View file

@ -1,79 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DateRangeInput matches snapshot 1`] = `
<div
className=""
>
<div
className="DateRangePicker DateRangePicker_1"
>
<div>
<div
className="DateRangePickerInput DateRangePickerInput_1"
>
<div
className="DateInput DateInput_1"
>
<input
aria-describedby="DateInput__screen-reader-message-bookingStartDate"
aria-label="today"
autoComplete="off"
className="DateInput_input DateInput_input_1"
disabled={false}
id="bookingStartDate"
name="bookingStartDate"
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="today"
readOnly={false}
required={false}
type="text"
value=""
/>
<p
className="DateInput_screenReaderMessage DateInput_screenReaderMessage_1"
id="DateInput__screen-reader-message-bookingStartDate"
>
FieldDateRangeInput.screenReaderInputMessage
</p>
</div>
<div
aria-hidden="true"
className="DateRangePickerInput_arrow DateRangePickerInput_arrow_1"
role="presentation"
>
<span />
</div>
<div
className="DateInput DateInput_1"
>
<input
aria-describedby="DateInput__screen-reader-message-bookingEndDate"
aria-label="tomorrow"
autoComplete="off"
className="DateInput_input DateInput_input_1"
disabled={false}
id="bookingEndDate"
name="bookingEndDate"
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="tomorrow"
readOnly={false}
required={false}
type="text"
value=""
/>
<p
className="DateInput_screenReaderMessage DateInput_screenReaderMessage_1"
id="DateInput__screen-reader-message-bookingEndDate"
>
FieldDateRangeInput.screenReaderInputMessage
</p>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -4,12 +4,9 @@ import * as validators from '../../util/validators';
import { Button } from '../../components';
import FieldPhoneNumberInput from './FieldPhoneNumberInput';
const formName = 'Styleguide.FieldPhoneNumberInput.Form';
const FormComponent = props => (
<FinalForm
{...props}
form={formName}
render={fieldRenderProps => {
const { formId, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps;
const required = validators.required('This field is required');
@ -41,7 +38,7 @@ const FormComponent = props => (
export const PhoneNumber = {
component: FormComponent,
props: {
formId: 'PhoneNumberExample',
formId: 'Styleguide.FieldPhoneNumberInput.Form',
onChange: formState => {
if (formState.dirty) {
console.log('form values changed to:', formState.values);

View file

@ -2,12 +2,10 @@ import React from 'react';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import { Button } from '..';
import FieldRadioButton from './FieldRadioButton';
const formName = 'Styleguide.FieldRadioButton.Form';
const FormComponent = props => (
<FinalForm
{...props}
form={formName}
render={fieldRenderProps => {
const {
handleSubmit,

View file

@ -10,9 +10,9 @@ const formName = 'Styleguide.FieldReviewRating.Form';
const FormComponent = props => (
<FinalForm
{...props}
form={formName}
formId={formName}
render={fieldRenderProps => {
const { form, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps;
const { formId, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps;
const required = validators.required('This field is required');
const submitDisabled = invalid || pristine || submitting;
@ -25,7 +25,7 @@ const FormComponent = props => (
>
<FormSpy onChange={onChange} />
<FieldReviewRating
id={`${form}.rate1`}
id={`${formId}.rate1`}
name="rating"
label="Rate your experience"
validate={required}

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { intlShape, injectIntl } from 'react-intl';
import { intlShape, injectIntl } from '../../util/reactIntl';
import { Field } from 'react-final-form';
import classNames from 'classnames';
import { IconReviewStar, ValidationError } from '../../components';

View file

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { func, object, shape, string } from 'prop-types';
import { bool, func, object, shape, string } from 'prop-types';
import { Field } from 'react-final-form';
import classNames from 'classnames';
import { ValidationError, ExpandingTextarea } from '../../components';
@ -18,10 +18,11 @@ class FieldTextInputComponent extends Component {
customErrorText,
id,
label,
type,
input,
meta,
onUnmount,
isUncontrolled,
inputRef,
...rest
} = this.props;
/* eslint-enable no-unused-vars */
@ -31,7 +32,7 @@ class FieldTextInputComponent extends Component {
}
const { valid, invalid, touched, error } = meta;
const isTextarea = type === 'textarea';
const isTextarea = input.type === 'textarea';
const errorText = customErrorText || error;
@ -41,6 +42,13 @@ class FieldTextInputComponent extends Component {
const fieldMeta = { touched: hasError, error: errorText };
// Textarea doesn't need type.
const { type, ...inputWithoutType } = input;
// Uncontrolled input uses defaultValue instead of value.
const { value: defaultValue, ...inputWithoutValue } = input;
// Use inputRef if it is passed as prop.
const refMaybe = inputRef ? { ref: inputRef } : {};
const inputClasses =
inputRootClass ||
classNames(css.input, {
@ -48,9 +56,28 @@ class FieldTextInputComponent extends Component {
[css.inputError]: hasError,
[css.textarea]: isTextarea,
});
const maxLength = CONTENT_MAX_LENGTH;
const inputProps = isTextarea
? { className: inputClasses, id, rows: 1, maxLength: CONTENT_MAX_LENGTH, ...input, ...rest }
: { className: inputClasses, id, type, ...input, ...rest };
? {
className: inputClasses,
id,
rows: 1,
maxLength,
...refMaybe,
...inputWithoutType,
...rest,
}
: isUncontrolled
? {
className: inputClasses,
id,
type,
defaultValue,
...refMaybe,
...inputWithoutValue,
...rest,
}
: { className: inputClasses, id, type, ...refMaybe, ...input, ...rest };
const classes = classNames(rootClassName || css.root, className);
return (
@ -71,6 +98,8 @@ FieldTextInputComponent.defaultProps = {
customErrorText: null,
id: null,
label: null,
isUncontrolled: false,
inputRef: null,
};
FieldTextInputComponent.propTypes = {
@ -89,12 +118,17 @@ FieldTextInputComponent.propTypes = {
id: string,
label: string,
// Either 'textarea' or something that is passed to the input element
type: string.isRequired,
// Uncontrolled input uses defaultValue prop, but doesn't pass value from form to the field.
// https://reactjs.org/docs/uncontrolled-components.html#default-values
isUncontrolled: bool,
// a ref object passed for input element.
inputRef: object,
// Generated by final-form's Field component
input: shape({
onChange: func.isRequired,
// Either 'textarea' or something that is passed to the input element
type: string.isRequired,
}).isRequired,
meta: object.isRequired,
};

View file

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { bool, func, node, object, string } from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl';
import { FilterForm } from '../../forms';
import css from './FilterPlain.css';

View file

@ -41,6 +41,11 @@
border: 1px solid var(--marketplaceColorDark);
}
}
.labelEllipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.popup {
/* By default hide the content */

View file

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { bool, func, node, number, object, string } from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from 'react-intl';
import { injectIntl, intlShape } from '../../util/reactIntl';
import { OutsideClickHandler } from '../../components';
import { FilterForm } from '../../forms';
@ -103,6 +103,7 @@ class FilterPopup extends Component {
popupClassName,
id,
label,
labelMaxWidth,
isSelected,
children,
initialValues,
@ -114,6 +115,8 @@ class FilterPopup extends Component {
const popupClasses = classNames(css.popup, { [css.isOpen]: this.state.isOpen });
const popupSizeClasses = popupClassName || css.popupSize;
const labelStyles = isSelected ? css.labelSelected : css.label;
const labelMaxWidthMaybe = labelMaxWidth ? { maxWidth: `${labelMaxWidth}px` } : {};
const labelMaxWidthStyles = labelMaxWidth ? css.labelEllipsis : null;
const contentStyle = this.positionStyleForContent();
return (
@ -125,7 +128,11 @@ class FilterPopup extends Component {
this.filter = node;
}}
>
<button className={labelStyles} onClick={() => this.toggleOpen()}>
<button
className={classNames(labelStyles, labelMaxWidthStyles)}
style={labelMaxWidthMaybe}
onClick={() => this.toggleOpen()}
>
{label}
</button>
<div
@ -167,6 +174,7 @@ FilterPopup.defaultProps = {
contentPlacementOffset: 0,
liveEdit: false,
label: null,
labelMaxWidth: null,
};
FilterPopup.propTypes = {
@ -180,6 +188,7 @@ FilterPopup.propTypes = {
keepDirtyOnReinitialize: bool,
contentPlacementOffset: number,
label: string.isRequired,
labelMaxWidth: number,
isSelected: bool.isRequired,
children: node.isRequired,

View file

@ -1,6 +1,6 @@
import React from 'react';
import { string } from 'prop-types';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl';
import classNames from 'classnames';
import { twitterPageURL } from '../../util/urlHelpers';
import config from '../../config';

View file

@ -6,6 +6,8 @@ import css from './IconArrowHead.css';
const DIRECTION_RIGHT = 'right';
const DIRECTION_LEFT = 'left';
const DIRECTION_DOWN = 'down';
const DIRECTION_UP = 'up';
const SIZE_BIG = 'big';
const SIZE_SMALL = 'small';
@ -15,6 +17,8 @@ const IconArrowHead = props => {
const isRight = direction === DIRECTION_RIGHT;
const isLeft = direction === DIRECTION_LEFT;
const isDown = direction === DIRECTION_DOWN;
const isUp = direction === DIRECTION_UP;
const isBig = size === SIZE_BIG;
const isSmall = size === SIZE_SMALL;
@ -48,6 +52,36 @@ const IconArrowHead = props => {
/>
</svg>
);
} else if (isDown && isSmall) {
return (
<svg
className={classes}
width="13"
height="9"
viewBox="0 0 13 9"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.03 6.805c.26.26.68.26.94 0l5.335-5.333a.668.668 0 0 0-.943-.943L6.5 5.39 1.638.53a.666.666 0 1 0-.943.942L6.03 6.805z"
fillRule="evenodd"
/>
</svg>
);
} else if (isUp && isSmall) {
return (
<svg
className={classes}
width="13"
height="9"
viewBox="0 0 13 9"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.97.195a.664.664 0 0 0-.94 0L.695 5.528a.668.668 0 0 0 .943.943L6.5 1.61l4.862 4.86a.666.666 0 1 0 .943-.942L6.97.195z"
fillRule="evenodd"
/>
</svg>
);
} else if (isRight && isBig) {
return (
<svg
@ -78,6 +112,36 @@ const IconArrowHead = props => {
/>
</svg>
);
} else if (isDown && isBig) {
return (
<svg
className={classes}
width="15"
height="11"
viewBox="0 0 15 11"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M.5 1.1C.5.93.565.76.692.642a.508.508 0 0 1 .764.07L7 7.972 12.545.71a.506.506 0 0 1 .763-.07c.23.214.257.592.064.846l-5.958 7.8A.524.524 0 0 1 7 9.498a.522.522 0 0 1-.414-.212l-5.958-7.8A.638.638 0 0 1 .5 1.098"
fillRule="evenodd"
/>
</svg>
);
} else if (isUp && isBig) {
return (
<svg
className={classes}
width="15"
height="11"
viewBox="0 0 15 11"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.5 8.9c0 .17-.065.34-.192.458a.508.508 0 0 1-.764-.07L7 2.028 1.455 9.29a.506.506 0 0 1-.763.07.644.644 0 0 1-.064-.846l5.958-7.8A.524.524 0 0 1 7 .502c.16 0 .31.077.414.212l5.958 7.8c.086.113.128.25.128.388"
fillRule="evenodd"
/>
</svg>
);
}
};
@ -90,7 +154,7 @@ IconArrowHead.defaultProps = {
IconArrowHead.propTypes = {
className: string,
rootClassName: string,
direction: oneOf([DIRECTION_RIGHT, DIRECTION_LEFT]).isRequired,
direction: oneOf([DIRECTION_RIGHT, DIRECTION_LEFT, DIRECTION_DOWN, DIRECTION_UP]).isRequired,
size: oneOf([SIZE_BIG, SIZE_SMALL]),
};

View file

@ -0,0 +1,4 @@
@import '../../marketplace.css';
.root {
}

File diff suppressed because one or more lines are too long

View file

@ -4,19 +4,38 @@ import classNames from 'classnames';
import css from './IconCheckMark.css';
const SIZE_SMALL = 'small';
const SIZE_BIG = 'big';
const IconCheckmark = props => {
const { rootClassName, className } = props;
const { rootClassName, className, size } = props;
const classes = classNames(rootClassName || css.root, className);
return (
<svg className={classes} strokeWidth="2" xmlns="http://www.w3.org/2000/svg">
<path d="M22.6 1.2c-.4-.3-1-.2-1.3.2L7.8 19l-5.2-5c-.4-.4-1-.4-1.3 0-.4.3-.4.8 0 1l6 5.6.6.2s.2 0 .4-.4l14.3-18c.3-.5.2-1-.2-1" />
</svg>
);
if (size === SIZE_SMALL) {
return (
<svg className={classes} width="16" height="12" xmlns="http://www.w3.org/2000/svg">
<path
d="M14 2l-8 8-4-4"
strokeWidth="2.5"
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
} else if (size === SIZE_BIG) {
return (
<svg className={classes} strokeWidth="2" xmlns="http://www.w3.org/2000/svg">
<path d="M22.6 1.2c-.4-.3-1-.2-1.3.2L7.8 19l-5.2-5c-.4-.4-1-.4-1.3 0-.4.3-.4.8 0 1l6 5.6.6.2s.2 0 .4-.4l14.3-18c.3-.5.2-1-.2-1" />
</svg>
);
}
};
IconCheckmark.defaultProps = {
rootClassName: null,
className: null,
size: 'big',
};
const { string } = PropTypes;
@ -24,6 +43,7 @@ const { string } = PropTypes;
IconCheckmark.propTypes = {
rootClassName: string,
className: string,
size: string,
};
export default IconCheckmark;

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from 'react-intl';
import { injectIntl, intlShape } from '../../util/reactIntl';
import classNames from 'classnames';
import { ResponsiveImage, IconSpinner } from '../../components';
import { propTypes } from '../../util/types';

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from '../../util/reactIntl';
import classNames from 'classnames';
import { Promised } from '../../components';

View file

@ -0,0 +1,65 @@
@import '../../marketplace.css';
.root {
position: relative;
display: inline-block;
}
.label {
@apply --marketplaceButtonStylesSecondary;
@apply --marketplaceSearchFilterLabelFontStyles;
padding: 9px 16px 10px 16px;
width: auto;
height: auto;
min-height: 0;
border-radius: 4px;
&:focus {
outline: none;
background-color: var(--matterColorLight);
border-color: transparent;
text-decoration: none;
box-shadow: var(--boxShadowFilterButton);
}
}
.labelSelected {
@apply --marketplaceButtonStyles;
@apply --marketplaceSearchFilterLabelFontStyles;
font-weight: var(--fontWeightSemiBold);
padding: 9px 16px 10px 16px;
width: auto;
height: auto;
min-height: 0;
border-radius: 4px;
border: 1px solid var(--marketplaceColor);
&:hover,
&:focus {
border: 1px solid var(--marketplaceColorDark);
}
}
.labelText {
display: inline-block;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.field {
@apply --marketplaceH4FontStyles;
margin: 0;
padding: 1px 0 13px 0;
border: none;
}
.fieldPlain {
@apply --marketplaceH4FontStyles;
margin: 0;
padding: 16px 0 30px 20px;
border: none;
}

View file

@ -0,0 +1,69 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import KeywordFilter from './KeywordFilter';
import { stringify, parse } from '../../util/urlHelpers';
const URL_PARAM = 'keywords';
const handleSubmit = (urlParam, values, history) => {
console.log('Submitting values', values);
const queryParams = values ? `?${stringify({ [urlParam]: values })}` : '';
history.push(`${window.location.pathname}${queryParams}`);
};
const KeywordFilterPopup = withRouter(props => {
const { history, location } = props;
const params = parse(location.search);
const keyword = params[URL_PARAM];
const initialValues = !!keyword ? keyword : '';
return (
<KeywordFilter
id="KeywordFilterPopupExample"
name="keyword"
urlParam={URL_PARAM}
label="Keyword"
onSubmit={(urlParam, values) => handleSubmit(urlParam, values, history)}
showAsPopup={true}
liveEdit={false}
initialValues={initialValues}
contentPlacementOffset={-14}
/>
);
});
export const KeywordFilterPopupExample = {
component: KeywordFilterPopup,
props: {},
group: 'filters',
};
const KeywordFilterPlain = withRouter(props => {
const { history, location } = props;
const params = parse(location.search);
const keyword = params[URL_PARAM];
const initialValues = !!keyword ? keyword : '';
return (
<KeywordFilter
id="KeywordFilterPlainExample"
name="keyword"
urlParam={URL_PARAM}
label="Keyword"
onSubmit={(urlParam, values) => {
handleSubmit(urlParam, values, history);
}}
showAsPopup={false}
liveEdit={true}
initialValues={initialValues}
/>
);
});
export const KeywordFilterPlainExample = {
component: KeywordFilterPlain,
props: {},
group: 'filters',
};

View file

@ -0,0 +1,214 @@
import React, { Component } from 'react';
import { func, number, string } from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from '../../util/reactIntl';
import debounce from 'lodash/debounce';
import { FieldTextInput } from '../../components';
import { FilterPopup, FilterPlain } from '../../components';
import css from './KeywordFilter.css';
// When user types, we wait for new keystrokes a while before searching new content
const DEBOUNCE_WAIT_TIME = 600;
// Short search queries (e.g. 2 letters) have a longer timeout before search is made
const TIMEOUT_FOR_SHORT_QUERIES = 2000;
class KeywordFilter extends Component {
constructor(props) {
super(props);
this.filter = null;
this.filterContent = null;
this.shortKeywordTimeout = null;
this.mobileInputRef = React.createRef();
this.positionStyleForContent = this.positionStyleForContent.bind(this);
}
componentWillUnmount() {
window.clearTimeout(this.shortKeywordTimeout);
}
positionStyleForContent() {
if (this.filter && this.filterContent) {
// Render the filter content to the right from the menu
// unless there's no space in which case it is rendered
// to the left
const distanceToRight = window.innerWidth - this.filter.getBoundingClientRect().right;
const labelWidth = this.filter.offsetWidth;
const contentWidth = this.filterContent.offsetWidth;
const contentWidthBiggerThanLabel = contentWidth - labelWidth;
const renderToRight = distanceToRight > contentWidthBiggerThanLabel;
const contentPlacementOffset = this.props.contentPlacementOffset;
const offset = renderToRight
? { left: contentPlacementOffset }
: { right: contentPlacementOffset };
// set a min-width if the content is narrower than the label
const minWidth = contentWidth < labelWidth ? { minWidth: labelWidth } : null;
return { ...offset, ...minWidth };
}
return {};
}
render() {
const {
rootClassName,
className,
id,
name,
label,
initialValues,
contentPlacementOffset,
onSubmit,
urlParam,
intl,
showAsPopup,
...rest
} = this.props;
const classes = classNames(rootClassName || css.root, className);
const hasInitialValues = !!initialValues && initialValues.length > 0;
const labelForPopup = hasInitialValues
? intl.formatMessage({ id: 'KeywordFilter.labelSelected' }, { labelText: initialValues })
: label;
const labelForPlain = hasInitialValues
? intl.formatMessage(
{ id: 'KeywordFilterPlainForm.labelSelected' },
{ labelText: initialValues }
)
: label;
const filterText = intl.formatMessage({ id: 'KeywordFilter.filterText' });
const placeholder = intl.formatMessage({ id: 'KeywordFilter.placeholder' });
const contentStyle = this.positionStyleForContent();
// pass the initial values with the name key so that
// they can be passed to the correct field
const namedInitialValues = { [name]: initialValues };
const handleSubmit = (urlParam, values) => {
const usedValue = values ? values[name] : values;
onSubmit(urlParam, usedValue);
};
const debouncedSubmit = debounce(handleSubmit, DEBOUNCE_WAIT_TIME, {
leading: false,
trailing: true,
});
// Use timeout for shart queries and debounce for queries with any length
const handleChangeWithDebounce = (urlParam, values) => {
// handleSubmit gets urlParam and values as params.
// If this field ("keyword") is short, create timeout
const hasKeywordValue = values && values[name];
const keywordValue = hasKeywordValue ? values && values[name] : '';
if (urlParam && (!hasKeywordValue || (hasKeywordValue && keywordValue.length >= 3))) {
if (this.shortKeywordTimeout) {
window.clearTimeout(this.shortKeywordTimeout);
}
return debouncedSubmit(urlParam, values);
} else {
this.shortKeywordTimeout = window.setTimeout(() => {
// if mobileInputRef exists, use the most up-to-date value from there
return this.mobileInputRef && this.mobileInputRef.current
? handleSubmit(urlParam, { ...values, [name]: this.mobileInputRef.current.value })
: handleSubmit(urlParam, values);
}, TIMEOUT_FOR_SHORT_QUERIES);
}
};
// Uncontrolled input needs to be cleared through the reference to DOM element.
const handleClear = () => {
if (this.mobileInputRef && this.mobileInputRef.current) {
this.mobileInputRef.current.value = '';
}
};
return showAsPopup ? (
<FilterPopup
className={classes}
rootClassName={rootClassName}
popupClassName={css.popupSize}
name={name}
label={labelForPopup}
isSelected={hasInitialValues}
id={`${id}.popup`}
showAsPopup
labelMaxWidth={250}
contentPlacementOffset={contentPlacementOffset}
onSubmit={handleSubmit}
initialValues={namedInitialValues}
urlParam={urlParam}
keepDirtyOnReinitialize
{...rest}
>
<FieldTextInput
className={css.field}
name={name}
id={`${id}-input`}
type="text"
label={filterText}
placeholder={placeholder}
autoComplete="off"
/>
</FilterPopup>
) : (
<FilterPlain
className={className}
rootClassName={rootClassName}
label={labelForPlain}
isSelected={hasInitialValues}
id={`${id}.plain`}
liveEdit
contentPlacementOffset={contentStyle}
onSubmit={handleChangeWithDebounce}
onClear={handleClear}
initialValues={namedInitialValues}
urlParam={urlParam}
{...rest}
>
<fieldset className={css.fieldPlain}>
<label>{filterText}</label>
<FieldTextInput
name={name}
id={`${id}-input`}
isUncontrolled
inputRef={this.mobileInputRef}
type="text"
placeholder={placeholder}
autoComplete="off"
/>
</fieldset>
</FilterPlain>
);
}
}
KeywordFilter.defaultProps = {
rootClassName: null,
className: null,
initialValues: null,
contentPlacementOffset: 0,
};
KeywordFilter.propTypes = {
rootClassName: string,
className: string,
id: string.isRequired,
name: string.isRequired,
urlParam: string.isRequired,
label: string.isRequired,
onSubmit: func.isRequired,
initialValues: string,
contentPlacementOffset: number,
// form injectIntl
intl: intlShape.isRequired,
};
export default injectIntl(KeywordFilter);

View file

@ -17,12 +17,12 @@
flex-direction: column;
width: 100%;
/* When side nav is shown as a column, Topbar's box-shadow should be visible */
z-index: calc(var(--zIndexTopbar) - 1);
@media (--viewportLarge) {
flex-direction: row;
justify-content: center;
/* When side nav is shown as a column, Topbar's box-shadow should be visible */
z-index: calc(var(--zIndexTopbar) - 1);
}
}

View file

@ -10,6 +10,7 @@ import classNames from 'classnames';
import {
LayoutWrapperTopbar,
LayoutWrapperSideNav,
LayoutWrapperAccountSettingsSideNav,
LayoutWrapperMain,
LayoutWrapperFooter,
} from '../../components';
@ -31,7 +32,7 @@ const prepareChildren = children => {
React.Children.forEach(children, child => {
if (child.type === LayoutWrapperTopbar) {
childrenMap.layoutWrapperTopbar = child;
} else if (child.type === LayoutWrapperSideNav) {
} else if ([LayoutWrapperSideNav, LayoutWrapperAccountSettingsSideNav].includes(child.type)) {
childrenMap.layoutWrapperSideNav = child;
} else if (child.type === LayoutWrapperMain) {
// LayoutWrapperMain needs different rootClassName

View file

@ -0,0 +1,107 @@
/**
* This is a wrapper component for different Layouts.
* Navigational 'aside' content should be added to this wrapper.
*/
import React from 'react';
import { node, number, string, shape } from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage } from '../../util/reactIntl';
import { withViewport } from '../../util/contextHelpers';
import { LayoutWrapperSideNav } from '../../components';
const MAX_HORIZONTAL_NAV_SCREEN_WIDTH = 1023;
const scrollToTab = currentTab => {
const el = document.querySelector(`#${currentTab}Tab`);
if (el) {
el.scrollIntoView({
block: 'end',
inline: 'end',
behavior: 'smooth',
});
}
};
const LayoutWrapperAccountSettingsSideNavComponent = props => {
const { currentTab, viewport } = props;
let hasScrolledToTab = false;
const { width } = viewport;
const hasViewport = width > 0;
const hasHorizontalTabLayout = hasViewport && width <= MAX_HORIZONTAL_NAV_SCREEN_WIDTH;
const hasVerticalTabLayout = hasViewport && width > MAX_HORIZONTAL_NAV_SCREEN_WIDTH;
const hasFontsLoaded = hasViewport && document.documentElement.classList.contains('fontsLoaded');
// Check if scrollToTab call is needed (tab is not visible on mobile)
if (hasVerticalTabLayout) {
hasScrolledToTab = true;
} else if (hasHorizontalTabLayout && !hasScrolledToTab && hasFontsLoaded) {
scrollToTab(currentTab);
hasScrolledToTab = true;
}
const tabs = [
{
text: <FormattedMessage id="LayoutWrapperAccountSettingsSideNav.contactDetailsTabTitle" />,
selected: currentTab === 'ContactDetailsPage',
id: 'ContactDetailsPageTab',
linkProps: {
name: 'ContactDetailsPage',
},
},
{
text: <FormattedMessage id="LayoutWrapperAccountSettingsSideNav.passwordTabTitle" />,
selected: currentTab === 'PasswordChangePage',
id: 'PasswordChangePageTab',
linkProps: {
name: 'PasswordChangePage',
},
},
{
text: <FormattedMessage id="LayoutWrapperAccountSettingsSideNav.paymentsTabTitle" />,
selected: currentTab === 'PayoutPreferencesPage',
id: 'PayoutPreferencesPageTab',
linkProps: {
name: 'PayoutPreferencesPage',
},
},
{
text: <FormattedMessage id="LayoutWrapperAccountSettingsSideNav.paymentMethodsTabTitle" />,
selected: currentTab === 'PaymentMethodsPage',
id: 'PaymentMethodsPageTab',
linkProps: {
name: 'PaymentMethodsPage',
},
},
];
return <LayoutWrapperSideNav tabs={tabs} />;
};
LayoutWrapperAccountSettingsSideNavComponent.defaultProps = {
className: null,
rootClassName: null,
children: null,
currentTab: null,
};
LayoutWrapperAccountSettingsSideNavComponent.propTypes = {
children: node,
className: string,
rootClassName: string,
currentTab: string,
// from withViewport
viewport: shape({
width: number.isRequired,
height: number.isRequired,
}).isRequired,
};
const LayoutWrapperAccountSettingsSideNav = compose(withViewport)(
LayoutWrapperAccountSettingsSideNavComponent
);
export default LayoutWrapperAccountSettingsSideNav;

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { string, func } from 'prop-types';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import { FormattedMessage, intlShape, injectIntl } from '../../util/reactIntl';
import classNames from 'classnames';
import { lazyLoadWithDimensions } from '../../util/contextHelpers';
import { LINE_ITEM_DAY, LINE_ITEM_NIGHT, propTypes } from '../../util/types';

View file

@ -4,6 +4,8 @@ import { propTypes } from '../../util/types';
import { Button } from '../../components';
import LocationAutocompleteInput from './LocationAutocompleteInput';
const identity = v => v;
const Form = props => {
return (
<FinalForm
@ -12,7 +14,7 @@ const Form = props => {
return (
<form onSubmit={handleSubmit}>
<label htmlFor="location">Select location:</label>
<Field name="location" format={null} component={LocationAutocompleteInput} />
<Field name="location" format={identity} component={LocationAutocompleteInput} />
<Button type="submit" style={{ marginTop: '24px' }} disabled={pristine}>
Submit
</Button>

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { any, arrayOf, bool, func, number, shape, string, oneOfType, object } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from '../../util/reactIntl';
import classNames from 'classnames';
import debounce from 'lodash/debounce';
import { IconSpinner } from '../../components';

View file

@ -10,13 +10,23 @@ const IconLogo = props => {
{...rest}
width="21"
height="25"
viewBox="0 0 21 25"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.063 23.646c-2.286 0-4.025-1.852-4.025-4.138 0-3.342 2.405-6.49 5.69-7.246-1.875 1.382-1.77 4.06-1.032 5.176.506-.416.956-.87 1.033-1.55 1.727.68 2.358 2.147 2.358 3.733 0 2.288-1.74 4.026-4.025 4.026m9.64-12.442c-.108-.352-.536-.467-.807-.24-.7.592-1.47 1.063-2.3 1.406-.21-6.144-4.754-11.247-10.8-12.214-.42-.066-.73.385-.514.753 1.53 2.598 1.656 5.74.443 8.395-.59-.816-1.033-1.717-1.32-2.686-.1-.342-.522-.482-.805-.244C1.313 8.29 0 11.1 0 14.086c0 5.62 4.553 10.063 10.063 10.063 5.548 0 10.062-4.514 10.062-10.063 0-.982-.143-1.952-.423-2.883"
fillRule="evenodd"
/>
<path d="M229.781,0c-81.569,0-147.93,63.047-147.93,140.543c0,54.028,20.928,84.706,41.178,114.388
c16.882,24.736,34.339,50.313,35.394,84.912c0.26,8.539,6.353,15.552,14.384,17.289c-7.475,2.291-12.912,9.244-12.912,17.47
c0,7.247,4.23,13.489,10.347,16.442c-6.116,2.954-10.347,9.196-10.347,16.444c0,10.091,8.18,18.271,18.271,18.271h11.167
c3.45,19.216,20.24,33.801,40.448,33.801c20.208,0,37-14.585,40.449-33.801h11.166c10.092,0,18.271-8.181,18.271-18.271
c0-7.248-4.23-13.49-10.348-16.444c6.117-2.953,10.348-9.195,10.348-16.442c0-8.189-5.389-15.12-12.812-17.44
c8.025-1.742,14.113-8.752,14.375-17.288c1.053-34.586,18.482-60.163,35.354-84.922c20.221-29.668,41.127-60.346,41.127-114.408
C377.71,63.047,311.351,0,229.781,0z M306.371,234.396c-15.518,22.771-34.314,50.355-40.014,86.645l-18.83-0.005l26.897-153.13
c0.015-0.083,0.028-0.166,0.042-0.249c1.43-9.385-1.296-18.896-7.479-26.093c-5.983-6.964-14.709-11.121-23.342-11.121
c-4.852,0-9.609,1.318-13.877,3.762c-4.269-2.444-9.024-3.762-13.877-3.762c-8.559,0-16.967,4.064-23.071,11.15
c-6.308,7.324-9.134,16.813-7.753,26.035c0.014,0.093,0.029,0.185,0.045,0.277l26.897,153.12l-18.714-0.006
c-5.705-36.312-24.54-63.911-40.091-86.696c-18.68-27.382-34.812-51.029-34.812-93.78c0-57.347,49.968-104.001,111.388-104.001
c61.419,0,111.388,46.654,111.388,104.001C341.169,183.337,325.05,206.987,306.371,234.396z M229.768,293.268l-22.669-129.053
c-0.537-3.92,1.643-6.947,2.612-8.071c1.783-2.069,4.208-3.406,6.18-3.406c2.472,0,4.369,1.851,5.28,2.953
c2.118,2.566,5.27,4.051,8.597,4.051c3.326,0,6.479-1.485,8.597-4.051c0.912-1.103,2.809-2.953,5.281-2.953
c2.189,0,4.653,1.286,6.43,3.354c1.133,1.319,2.936,4.115,2.365,8.103L229.768,293.268z"/>
</svg>
);
};

View file

@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import config from '../../config';
import IconLogo from './IconLogo';
import LogoImage from './saunatime-logo.png';
// import IconLogo from './IconLogo';
import LogoImage from './inventionpad-logo.png';
import css from './Logo.css';
const Logo = props => {
@ -15,7 +15,7 @@ const Logo = props => {
return <img className={className} src={LogoImage} alt={config.siteTitle} {...rest} />;
}
return <IconLogo className={mobileClasses} {...rest} />;
return <img src={LogoImage} width="80px" height="30px" alt={config.siteTitle} className={mobileClasses} {...rest} />;
};
const { oneOf, string } = PropTypes;

Some files were not shown because too many files have changed in this diff Show more