Skip to content

Commit

Permalink
Merge pull request #191 from sharetribe/update-v8.5.0-from-upstream
Browse files Browse the repository at this point in the history
Update v10.5.0 (update from upstream aka FTW-daily v8.5.0)
  • Loading branch information
Gnito authored May 16, 2022
2 parents cdb22a0 + 0d720de commit 1689e03
Show file tree
Hide file tree
Showing 20 changed files with 405 additions and 213 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,29 @@ https://github.com/sharetribe/flex-template-web/

## Upcoming version 2022-XX-XX

## [v10.5.0] 2022-05-16

- Add support for hosted translations. (PR made in upstream repo: FTW-daily)

- [delete] Remove old unused translation keys.
[#190](https://github.com/sharetribe/ftw-hourly/pull/190)

### Updates from upstream (FTW-daily v8.5.0)

- [add] Add support for hosted translations.

- This PR fetches "content/translation.json" from a new Asset Delivery API. The file is editable
through the Flex Console.
- It also adds all the missing translation keys to existing non-English translation files. This
means that those files might now include messages in English.

[#1510](https://github.com/sharetribe/ftw-daily/pull/1510)

- [delete] Remove old unused translation keys.
[#1511](https://github.com/sharetribe/ftw-daily/pull/1511)

[v10.5.0]: https://github.com/sharetribe/ftw-hourly/compare/v10.4.1...v10.5.0

## [v10.4.1] 2022-03-16

- [fix] FieldDateInput: input font-size must be min 16px for mobile. EditListingPage.duck: fix image
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "app",
"version": "10.4.1",
"version": "10.5.0",
"private": true,
"license": "Apache-2.0",
"dependencies": {
Expand Down Expand Up @@ -61,7 +61,7 @@
"redux": "^4.1.2",
"redux-thunk": "^2.4.1",
"seedrandom": "^3.0.5",
"sharetribe-flex-sdk": "^1.15.0",
"sharetribe-flex-sdk": "^1.17.0",
"sharetribe-scripts": "5.0.1",
"smoothscroll-polyfill": "^0.4.0",
"source-map-support": "^0.5.21",
Expand Down
6 changes: 6 additions & 0 deletions server/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ const data = 'data:';
const blob = 'blob:';
const devImagesMaybe = dev ? ['*.localhost:8000'] : [];
const baseUrl = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL || 'https://flex-api.sharetribe.com';
// Asset Delivery API is using a different domain than other Flex APIs
// cdn.st-api.com
// If assetCdnBaseUrl is used to initialize SDK (for proxy purposes), then that URL needs to be in CSP
const assetCdnBaseUrl = process.env.REACT_APP_SHARETRIBE_SDK_ASSET_CDN_BASE_URL;

// Default CSP whitelist.
//
Expand All @@ -20,6 +24,8 @@ const defaultDirectives = {
connectSrc: [
self,
baseUrl,
assetCdnBaseUrl,
'*.st-api.com',
'maps.googleapis.com',
'*.tiles.mapbox.com',
'api.mapbox.com',
Expand Down
17 changes: 14 additions & 3 deletions server/dataLoader.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const url = require('url');
const log = require('./log');

exports.loadData = function(requestUrl, sdk, matchPathname, configureStore, routeConfiguration) {
exports.loadData = function(requestUrl, sdk, appInfo) {
const { matchPathname, configureStore, routeConfiguration, config, fetchAppAssets } = appInfo;
const { pathname, query } = url.parse(requestUrl);
const matchedRoutes = matchPathname(pathname, routeConfiguration());

let translations = {};
const store = configureStore({}, sdk);

const dataLoadingCalls = matchedRoutes.reduce((calls, match) => {
Expand All @@ -15,9 +17,18 @@ exports.loadData = function(requestUrl, sdk, matchPathname, configureStore, rout
return calls;
}, []);

return Promise.all(dataLoadingCalls)
// First fetch app-wide assets
// Then make loadData calls
// And return object containing preloaded state and translations
// This order supports other asset (in the future) that should be fetched before data calls.
return store
.dispatch(fetchAppAssets(config.appCdnAssets))
.then(fetchedAssets => {
translations = fetchedAssets?.translations?.data || {};
return Promise.all(dataLoadingCalls);
})
.then(() => {
return store.getState();
return { preloadedState: store.getState(), translations };
})
.catch(e => {
log.error(e, 'server-side-data-load-failed');
Expand Down
11 changes: 7 additions & 4 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const dev = process.env.REACT_APP_ENV === 'development';
const PORT = parseInt(process.env.PORT, 10);
const CLIENT_ID = process.env.REACT_APP_SHARETRIBE_SDK_CLIENT_ID;
const BASE_URL = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL;
const ASSET_CDN_BASE_URL = process.env.REACT_APP_SHARETRIBE_SDK_ASSET_CDN_BASE_URL;
const TRANSIT_VERBOSE = process.env.REACT_APP_SHARETRIBE_SDK_TRANSIT_VERBOSE === 'true';
const USING_SSL = process.env.REACT_APP_SHARETRIBE_USING_SSL === 'true';
const TRUST_PROXY = process.env.SERVER_SHARETRIBE_TRUST_PROXY || null;
Expand Down Expand Up @@ -199,6 +200,7 @@ app.get('*', (req, res) => {
});

const baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {};
const assetCdnBaseUrl = ASSET_CDN_BASE_URL ? { assetCdnBaseUrl: ASSET_CDN_BASE_URL } : {};

const sdk = sharetribeSdk.createInstance({
transitVerbose: TRANSIT_VERBOSE,
Expand All @@ -208,6 +210,7 @@ app.get('*', (req, res) => {
tokenStore,
typeHandlers: sdkUtils.typeHandlers,
...baseUrl,
...assetCdnBaseUrl,
});

// Until we have a better plan for caching dynamic content and we
Expand All @@ -221,12 +224,12 @@ app.get('*', (req, res) => {

// Server-side entrypoint provides us the functions for server-side data loading and rendering
const nodeEntrypoint = nodeExtractor.requireEntrypoint();
const { default: renderApp, matchPathname, configureStore, routeConfiguration } = nodeEntrypoint;
const { default: renderApp, ...appInfo } = nodeEntrypoint;

dataLoader
.loadData(req.url, sdk, matchPathname, configureStore, routeConfiguration)
.then(preloadedState => {
const html = renderer.render(req.url, context, preloadedState, renderApp, webExtractor);
.loadData(req.url, sdk, appInfo)
.then(data => {
const html = renderer.render(req.url, context, data, renderApp, webExtractor);

if (dev) {
const debugData = {
Expand Down
13 changes: 11 additions & 2 deletions server/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,20 @@ const replacer = (key = null, value) => {
return types.replacer(key, cleanedValue);
};

exports.render = function(requestUrl, context, preloadedState, renderApp, webExtractor) {
exports.render = function(requestUrl, context, data, renderApp, webExtractor) {
const { preloadedState, translations } = data;

// Bind webExtractor as "this" for collectChunks call.
const collectWebChunks = webExtractor.collectChunks.bind(webExtractor);

const { head, body } = renderApp(requestUrl, context, preloadedState, collectWebChunks);
// Render the app with given route, preloaded state, hosted translations.
const { head, body } = renderApp(
requestUrl,
context,
preloadedState,
translations,
collectWebChunks
);

// Preloaded state needs to be passed for client side too.
// For security reasons we ensure that preloaded state is considered as a string
Expand Down
62 changes: 43 additions & 19 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import routeConfiguration from './routeConfiguration';
import Routes from './Routes';
import config from './config';

// Flex template application uses English translations as default.
// Flex template application uses English translations as default translations.
import defaultMessages from './translations/en.json';

// If you want to change the language, change the imports to match the wanted locale:
// If you want to change the language of default (fallback) translations,
// change the imports to match the wanted locale:
//
// 1) Change the language in the config.js file!
// 2) Import correct locale rules for Moment library
// 3) Use the `messagesInLocale` import to add the correct translation file.
Expand All @@ -48,6 +50,11 @@ const messagesInLocale = {};
const addMissingTranslations = (sourceLangTranslations, targetLangTranslations) => {
const sourceKeys = Object.keys(sourceLangTranslations);
const targetKeys = Object.keys(targetLangTranslations);

// if there's no translations defined for target language, return source translations
if (targetKeys.length === 0) {
return sourceLangTranslations;
}
const missingKeys = difference(sourceKeys, targetKeys);

const addMissingTranslation = (translations, missingKey) => ({
Expand All @@ -58,18 +65,15 @@ const addMissingTranslations = (sourceLangTranslations, targetLangTranslations)
return missingKeys.reduce(addMissingTranslation, targetLangTranslations);
};

const isDefaultLanguageInUse = config.locale === 'en';

const messages = isDefaultLanguageInUse
? defaultMessages
: addMissingTranslations(defaultMessages, messagesInLocale);

// Get default messages for a given locale.
//
// Note: Locale should not affect the tests. We ensure this by providing
// messages with the key as the value of each message and discard the value.
// { 'My.translationKey1': 'My.translationKey1', 'My.translationKey2': 'My.translationKey2' }
const isTestEnv = process.env.NODE_ENV === 'test';

// Locale should not affect the tests. We ensure this by providing
// messages with the key as the value of each message.
const testMessages = mapValues(messages, (val, key) => key);
const localeMessages = isTestEnv ? testMessages : messages;
const localeMessages = isTestEnv
? mapValues(defaultMessages, (val, key) => key)
: addMissingTranslations(defaultMessages, messagesInLocale);

const setupLocale = () => {
if (isTestEnv) {
Expand All @@ -85,10 +89,14 @@ const setupLocale = () => {
};

export const ClientApp = props => {
const { store } = props;
const { store, hostedTranslations = {} } = props;
setupLocale();
return (
<IntlProvider locale={config.locale} messages={localeMessages} textComponent="span">
<IntlProvider
locale={config.locale}
messages={{ ...localeMessages, ...hostedTranslations }}
textComponent="span"
>
<Provider store={store}>
<HelmetProvider>
<BrowserRouter>
Expand All @@ -105,11 +113,15 @@ const { any, string } = PropTypes;
ClientApp.propTypes = { store: any.isRequired };

export const ServerApp = props => {
const { url, context, helmetContext, store } = props;
const { url, context, helmetContext, store, hostedTranslations = {} } = props;
setupLocale();
HelmetProvider.canUseDOM = false;
return (
<IntlProvider locale={config.locale} messages={localeMessages} textComponent="span">
<IntlProvider
locale={config.locale}
messages={{ ...localeMessages, ...hostedTranslations }}
textComponent="span"
>
<Provider store={store}>
<HelmetProvider context={helmetContext}>
<StaticRouter location={url} context={context}>
Expand All @@ -133,7 +145,13 @@ ServerApp.propTypes = { url: string.isRequired, context: any.isRequired, store:
* - {String} body: Rendered application body of the given route
* - {Object} head: Application head metadata from react-helmet
*/
export const renderApp = (url, serverContext, preloadedState, collectChunks) => {
export const renderApp = (
url,
serverContext,
preloadedState,
hostedTranslations,
collectChunks
) => {
// Don't pass an SDK instance since we're only rendering the
// component tree with the preloaded store state and components
// shouldn't do any SDK calls in the (server) rendering lifecycle.
Expand All @@ -145,7 +163,13 @@ export const renderApp = (url, serverContext, preloadedState, collectChunks) =>
// This is needed to figure out correct chunks/scripts to be included to server-rendered page.
// https://loadable-components.com/docs/server-side-rendering/#3-setup-chunkextractor-server-side
const WithChunks = collectChunks(
<ServerApp url={url} context={serverContext} helmetContext={helmetContext} store={store} />
<ServerApp
url={url}
context={serverContext}
helmetContext={helmetContext}
store={store}
hostedTranslations={hostedTranslations}
/>
);
const body = ReactDOMServer.renderToString(WithChunks);
const { helmet: head } = helmetContext;
Expand Down
13 changes: 9 additions & 4 deletions src/components/SectionHero/SectionHero.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { string } from 'prop-types';
import { FormattedMessage } from '../../util/reactIntl';
import classNames from 'classnames';
Expand All @@ -7,17 +7,22 @@ import { NamedLink } from '../../components';
import css from './SectionHero.module.css';

const SectionHero = props => {
const [mounted, setMounted] = useState(false);
const { rootClassName, className } = props;

useEffect(() => {
setMounted(true);
}, []);

const classes = classNames(rootClassName || css.root, className);

return (
<div className={classes}>
<div className={css.heroContent}>
<h1 className={css.heroMainTitle}>
<h1 className={classNames(css.heroMainTitle, { [css.heroMainTitleFEDelay]: mounted })}>
<FormattedMessage id="SectionHero.title" />
</h1>
<h2 className={css.heroSubTitle}>
<h2 className={classNames(css.heroSubTitle, { [css.heroSubTitleFEDelay]: mounted })}>
<FormattedMessage id="SectionHero.subTitle" />
</h2>
<NamedLink
Expand All @@ -26,7 +31,7 @@ const SectionHero = props => {
search:
'address=United%20States%20of%20America&bounds=71.540724%2C-66.885444%2C18.765563%2C-179.9',
}}
className={css.heroButton}
className={classNames(css.heroButton, { [css.heroButtonFEDelay]: mounted })}
>
<FormattedMessage id="SectionHero.browseButton" />
</NamedLink>
Expand Down
16 changes: 10 additions & 6 deletions src/components/SectionHero/SectionHero.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
animation-duration: 0.5s;
animation-timing-function: ease-out;
-webkit-animation-fill-mode: forwards;
animation-delay: 3s;

visibility: hidden;
opacity: 1;
Expand Down Expand Up @@ -61,38 +62,41 @@
.heroMainTitle {
@apply --marketplaceHeroTitleFontStyles;
color: var(--matterColorLight);

composes: animation;
animation-delay: 0.5s;

@media (--viewportMedium) {
max-width: var(--SectionHero_desktopTitleMaxWidth);
}
}
.heroMainTitleFEDelay {
animation-delay: 0s;
}

.heroSubTitle {
@apply --marketplaceH4FontStyles;

color: var(--matterColorLight);
margin: 0 0 32px 0;

composes: animation;
animation-delay: 0.65s;

@media (--viewportMedium) {
max-width: var(--SectionHero_desktopTitleMaxWidth);
margin: 0 0 47px 0;
}
}
.heroSubTitleFEDelay {
animation-delay: 0.15s;
}

.heroButton {
@apply --marketplaceButtonStyles;
composes: animation;

animation-delay: 0.8s;

@media (--viewportMedium) {
display: block;
width: 260px;
}
}
.heroButtonFEDelay {
animation-delay: 0.3s;
}
Loading

0 comments on commit 1689e03

Please sign in to comment.