diff --git a/CHANGELOG.md b/CHANGELOG.md index c3fe34bba..0fd227390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,25 @@ way to update this template, but currently, we follow a pattern: --- -## Upcoming version 2022-XX-XX +## Upcoming version 2023-XX-XX + +## [v11.0.0] 2023-02-14 + +### Updates from upstream (FTW-daily v10.0.0) + +- [add] This adds support for page asset files that can be created in Console. These asset files are + taken into use for + + - LandingPage + - TermsOfServicePage + - PrivacyPolicyPage + - AboutPage + - and other static pages can also be created through Console (they'll be visible in route: + /p/:asset-name/) + + [#1520](https://github.com/sharetribe/ftw-daily/pull/1520) + + [v11.0.0]: https://github.com/sharetribe/ftw-product/compare/v10.1.0.../v11.0.0 ## [v10.1.0] 2023-02-07 diff --git a/package.json b/package.json index dd95aba64..c4d55bdd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "10.1.0", + "version": "11.0.0", "private": true, "license": "Apache-2.0", "dependencies": { @@ -54,11 +54,16 @@ "react-with-direction": "^1.4.0", "redux": "^4.2.0", "redux-thunk": "^2.4.1", + "rehype-react": "^6.2.1", + "rehype-sanitize": "^4.0.0", + "remark-parse": "^9.0.0", + "remark-rehype": "^8.1.0", "seedrandom": "^3.0.5", "sharetribe-flex-sdk": "^1.17.0", "sharetribe-scripts": "6.0.1", "smoothscroll-polyfill": "^0.4.0", "source-map-support": "^0.5.21", + "unified": "^9.2.2", "url": "^0.11.0" }, "devDependencies": { diff --git a/server/csp.js b/server/csp.js index 8139522d3..fa5dfe6c7 100644 --- a/server/csp.js +++ b/server/csp.js @@ -40,7 +40,7 @@ const defaultDirectives = { '*.stripe.com', ], fontSrc: [self, data, 'assets-sharetribecom.sharetribe.com', 'fonts.gstatic.com'], - frameSrc: [self, '*.stripe.com'], + frameSrc: [self, '*.stripe.com', '*.youtube-nocookie.com'], imgSrc: [ self, data, @@ -50,6 +50,8 @@ const defaultDirectives = { 'sharetribe.imgix.net', // Safari 9.1 didn't recognize asterisk rule. // Styleguide placeholder images + 'picsum.photos', + '*.picsum.photos', 'via.placeholder.com', 'api.mapbox.com', @@ -64,6 +66,9 @@ const defaultDirectives = { 'www.google-analytics.com', 'stats.g.doubleclick.net', + // Youtube (static image) + '*.ytimg.com', + '*.stripe.com', ], scriptSrc: [ diff --git a/server/dataLoader.js b/server/dataLoader.js index 65595753f..1f2687eb2 100644 --- a/server/dataLoader.js +++ b/server/dataLoader.js @@ -9,13 +9,14 @@ exports.loadData = function(requestUrl, sdk, appInfo) { let translations = {}; const store = configureStore({}, sdk); - const dataLoadingCalls = matchedRoutes.reduce((calls, match) => { - const { route, params } = match; - if (typeof route.loadData === 'function' && !route.auth) { - calls.push(store.dispatch(route.loadData(params, query))); - } - return calls; - }, []); + const dataLoadingCalls = () => + matchedRoutes.reduce((calls, match) => { + const { route, params } = match; + if (typeof route.loadData === 'function' && !route.auth) { + calls.push(store.dispatch(route.loadData(params, query))); + } + return calls; + }, []); // First fetch app-wide assets // Then make loadData calls @@ -23,9 +24,9 @@ exports.loadData = function(requestUrl, sdk, appInfo) { // 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(fetchedAppAssets => { + translations = fetchedAppAssets?.translations?.data || {}; + return Promise.all(dataLoadingCalls()); }) .then(() => { return { preloadedState: store.getState(), translations }; diff --git a/src/app.js b/src/app.js index a4322558c..59711c9db 100644 --- a/src/app.js +++ b/src/app.js @@ -93,6 +93,8 @@ const setupLocale = () => { export const ClientApp = props => { const { store, hostedTranslations = {} } = props; setupLocale(); + // This gives good input for debugging issues on live environments, but with test it's not needed. + const logLoadDataCalls = config?.env !== 'test'; return ( { - + diff --git a/src/app.test.js b/src/app.test.js index 4229f3fea..0edac8582 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -14,9 +14,22 @@ afterAll(() => { }); describe('Application - JSDOM environment', () => { - it('renders in the client without crashing', () => { + it('renders the LandingPage without crashing', () => { window.google = { maps: {} }; - const store = configureStore(); + + // LandingPage gets rendered and it calls hostedAsset > fetchPageAssets > sdk.assetByVersion + const pageData = { + data: { + sections: [], + _schema: './schema.json', + }, + meta: { + version: 'bCsMYVYVawc8SMPzZWJpiw', + }, + }; + const resolvePageAssetCall = () => Promise.resolve(pageData); + const fakeSdk = { assetByVersion: resolvePageAssetCall, assetByAlias: resolvePageAssetCall }; + const store = configureStore({}, fakeSdk); const div = document.createElement('div'); ReactDOM.render(, div); delete window.google; diff --git a/src/components/Avatar/Avatar.example.js b/src/components/Avatar/Avatar.example.js index e4a33d273..ed07caaaf 100644 --- a/src/components/Avatar/Avatar.example.js +++ b/src/components/Avatar/Avatar.example.js @@ -46,13 +46,13 @@ const userWithProfileImage = { name: 'square-small', width: 240, height: 240, - url: 'https://via.placeholder.com/240x240', + url: 'https://picsum.photos/240/240/', }, 'square-small2x': { name: 'square-small2x', width: 480, height: 480, - url: 'https://via.placeholder.com/480x480', + url: 'https://picsum.photos/480/480/', }, }, }, diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js index 8e6fb7158..35bf9a21c 100644 --- a/src/components/Footer/Footer.js +++ b/src/components/Footer/Footer.js @@ -87,7 +87,7 @@ const Footer = props => {
  • - +
  • @@ -102,7 +102,12 @@ const Footer = props => {
  • - +
  • diff --git a/src/components/Footer/Footer.module.css b/src/components/Footer/Footer.module.css index e9ceac080..cde6f828e 100644 --- a/src/components/Footer/Footer.module.css +++ b/src/components/Footer/Footer.module.css @@ -1,6 +1,7 @@ @import '../../styles/customMediaQueries.css'; .root { + position: relative; border-top-style: solid; border-top-width: 1px; border-top-color: var(--matterColorNegative); diff --git a/src/components/Page/Page.js b/src/components/Page/Page.js index 4443a903b..864f24a7b 100644 --- a/src/components/Page/Page.js +++ b/src/components/Page/Page.js @@ -80,11 +80,12 @@ class PageComponent extends Component { scrollingDisabled, referrer, author, - contentType, + openGraphType, description, facebookImages, published, schema, + socialSharing, tags, title, twitterHandle, @@ -97,7 +98,6 @@ class PageComponent extends Component { }); this.scrollingDisabledChanged(scrollingDisabled); - const referrerMeta = referrer ? : null; const canonicalRootURL = config.canonicalRootURL; const shouldReturnPathOnly = referrer && referrer !== 'unsafe-url'; @@ -107,9 +107,17 @@ class PageComponent extends Component { const siteTitle = config.siteTitle; const schemaTitle = intl.formatMessage({ id: 'Page.schemaTitle' }, { siteTitle }); const schemaDescription = intl.formatMessage({ id: 'Page.schemaDescription' }); - const metaTitle = title || schemaTitle; - const metaDescription = description || schemaDescription; - const facebookImgs = facebookImages || [ + const pageTitle = title || schemaTitle; + const pageDescription = description || schemaDescription; + const { + title: socialSharingTitle, + description: socialSharingDescription, + images1200: socialSharingImages1200, + // Note: we use image with open graph's aspect ratio (1.91:1) also with Twitter + images600: socialSharingImages600, + } = socialSharing || {}; + + const openGraphFallbackImages = [ { name: 'facebook', url: `${canonicalRootURL}${facebookImage}`, @@ -117,7 +125,7 @@ class PageComponent extends Component { height: 630, }, ]; - const twitterImgs = twitterImages || [ + const twitterFallbackImages = [ { name: 'twitter', url: `${canonicalRootURL}${twitterImage}`, @@ -125,25 +133,25 @@ class PageComponent extends Component { height: 314, }, ]; + const facebookImgs = socialSharingImages1200 || facebookImages || openGraphFallbackImages; + const twitterImgs = socialSharingImages600 || twitterImages || twitterFallbackImages; const metaToHead = metaTagProps({ author, - contentType, - description: metaDescription, + openGraphType, + socialSharingTitle: socialSharingTitle || pageTitle, + socialSharingDescription: socialSharingDescription || pageDescription, + description: pageDescription, facebookImages: facebookImgs, twitterImages: twitterImgs, published, tags, - title: metaTitle, twitterHandle, updated, url: canonicalUrl, locale: intl.locale, }); - // eslint-disable-next-line react/no-array-index-key - const metaTags = metaToHead.map((metaProps, i) => ); - const facebookPage = config.siteFacebookPage; const twitterPage = twitterPageURL(config.siteTwitterHandle); const instagramPage = config.siteInstagramPage; @@ -156,7 +164,8 @@ class PageComponent extends Component { // Schema attribute can be either single schema object or an array of objects // This makes it possible to include several different items from the same page. // E.g. Product, Place, Video - const schemaFromProps = Array.isArray(schema) ? schema : [schema]; + const hasSchema = schema != null; + const schemaFromProps = hasSchema && Array.isArray(schema) ? schema : hasSchema ? [schema] : []; const schemaArrayJSONString = JSON.stringify([ ...schemaFromProps, { @@ -175,9 +184,6 @@ class PageComponent extends Component { url: canonicalRootURL, description: schemaDescription, name: schemaTitle, - publisher: { - '@id': `${canonicalRootURL}#organization`, - }, }, ]); @@ -201,12 +207,14 @@ class PageComponent extends Component { lang: intl.locale, }} > - {title} - {referrerMeta} + {pageTitle} + {referrer ? : null} - {metaTags} + {metaToHead.map((metaProps, i) => ( + + ))} @@ -233,13 +241,14 @@ PageComponent.defaultProps = { rootClassName: null, children: null, author: null, - contentType: 'website', + openGraphType: 'website', description: null, facebookImages: null, twitterImages: null, published: null, referrer: null, schema: null, + socialSharing: null, tags: null, twitterHandle: null, updated: null, @@ -256,7 +265,7 @@ PageComponent.propTypes = { // SEO related props author: string, - contentType: string, // og:type + openGraphType: string, // og:type description: string, // page description facebookImages: arrayOf( shape({ @@ -274,8 +283,28 @@ PageComponent.propTypes = { ), published: string, // article:published_time schema: oneOfType([object, array]), // http://schema.org + socialSharing: shape({ + title: string, + description: string, + images1200: arrayOf( + // Page asset file can define this + shape({ + width: number.isRequired, + height: number.isRequired, + url: string.isRequired, + }) + ), + images600: arrayOf( + // Page asset file can define this + shape({ + width: number.isRequired, + height: number.isRequired, + url: string.isRequired, + }) + ), + }), tags: string, // article:tag - title: string.isRequired, // page title + title: string, // page title twitterHandle: string, // twitter handle updated: string, // article:modified_time diff --git a/src/components/PrivacyPolicy/PrivacyPolicy.js b/src/components/PrivacyPolicy/PrivacyPolicy.js deleted file mode 100644 index 1ab5289c0..000000000 --- a/src/components/PrivacyPolicy/PrivacyPolicy.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import css from './PrivacyPolicy.module.css'; - -const PrivacyPolicy = props => { - const { rootClassName, className } = props; - const classes = classNames(rootClassName || css.root, className); - - // prettier-ignore - return ( -
    -

    Last updated: July 1st, 2021

    - -

    - Thank you for using Sneakertime! Every marketplace business needs Terms of Service and - Privacy Policy agreements. To help you launch your marketplace faster, we've compiled - two templates you can use as a baseline for the agreements between your online marketplace - business and its users. You can access these templates at - https://www.sharetribe.com/docs/operator-guides/free-templates/ -

    - -

    1 Lorem ipsum dolor sit amet

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco - laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in - voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat - cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -

    - -

    2 Sed ut perspiciatis unde

    -

    - Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque - laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi - architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit - aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione - voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, - consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et - dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum - exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi - consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil - molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? -

    - -

    3 At vero eos et accusamus

    -

    - At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium - voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati - cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id - est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam - libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod - maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. - Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut - et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a - sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis - doloribus asperiores repellat -

    -
    - ); -}; - -PrivacyPolicy.defaultProps = { - rootClassName: null, - className: null, -}; - -const { string } = PropTypes; - -PrivacyPolicy.propTypes = { - rootClassName: string, - className: string, -}; - -export default PrivacyPolicy; diff --git a/src/components/PrivacyPolicy/PrivacyPolicy.module.css b/src/components/PrivacyPolicy/PrivacyPolicy.module.css deleted file mode 100644 index 16e59ac55..000000000 --- a/src/components/PrivacyPolicy/PrivacyPolicy.module.css +++ /dev/null @@ -1,39 +0,0 @@ -@import '../../styles/customMediaQueries.css'; - -.root { - & p { - font-weight: var(--fontWeightMedium); - font-size: 15px; - line-height: 24px; - letter-spacing: 0; - /* margin-top + n * line-height + margin-bottom => x * 6px */ - margin-top: 12px; - margin-bottom: 12px; - - @media (--viewportMedium) { - font-weight: var(--fontWeightMedium); - /* margin-top + n * line-height + margin-bottom => x * 8px */ - margin-top: 17px; - margin-bottom: 15px; - } - } - & h2 { - /* Adjust heading margins to work with the reduced body font size */ - margin: 29px 0 13px 0; - - @media (--viewportMedium) { - margin: 32px 0 0 0; - } - } -} - -.lastUpdated { - composes: marketplaceBodyFontStyles from global; - margin-top: 0; - margin-bottom: 55px; - - @media (--viewportMedium) { - margin-top: 0; - margin-bottom: 54px; - } -} diff --git a/src/components/ResponsiveImage/ResponsiveImage.js b/src/components/ResponsiveImage/ResponsiveImage.js index b27ab4b41..7fa8be31f 100644 --- a/src/components/ResponsiveImage/ResponsiveImage.js +++ b/src/components/ResponsiveImage/ResponsiveImage.js @@ -34,7 +34,7 @@ */ import React from 'react'; -import { arrayOf, string } from 'prop-types'; +import { arrayOf, oneOfType, string } from 'prop-types'; import classNames from 'classnames'; import { FormattedMessage } from '../../util/reactIntl'; import { propTypes } from '../../util/types'; @@ -104,7 +104,7 @@ ResponsiveImage.propTypes = { className: string, rootClassName: string, alt: string.isRequired, - image: propTypes.image, + image: oneOfType([propTypes.image, propTypes.imageAsset]), variants: arrayOf(string).isRequired, noImageMessage: string, }; diff --git a/src/components/TermsOfService/TermsOfService.js b/src/components/TermsOfService/TermsOfService.js deleted file mode 100644 index cab720057..000000000 --- a/src/components/TermsOfService/TermsOfService.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import css from './TermsOfService.module.css'; - -const TermsOfService = props => { - const { rootClassName, className } = props; - const classes = classNames(rootClassName || css.root, className); - - // prettier-ignore - return ( -
    -

    Last updated: July, 1st 2021

    - -

    - Thank you for using Sneakertime! Every marketplace business needs Terms of Service and - Privacy Policy agreements. To help you launch your marketplace faster, we've compiled - two templates you can use as a baseline for the agreements between your online marketplace - business and its users. You can access these templates at - https://www.sharetribe.com/docs/operator-guides/free-templates/ -

    - -

    1 Lorem ipsum dolor sit amet

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco - laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in - voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat - cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -

    - -

    2 Sed ut perspiciatis unde

    -

    - Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque - laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi - architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit - aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione - voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, - consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et - dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum - exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi - consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil - molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? -

    - -

    3 At vero eos et accusamus

    -

    - At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium - voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati - cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id - est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam - libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod - maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. - Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut - et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a - sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis - doloribus asperiores repellat -

    -
    - ); -}; - -TermsOfService.defaultProps = { - rootClassName: null, - className: null, -}; - -const { string } = PropTypes; - -TermsOfService.propTypes = { - rootClassName: string, - className: string, -}; - -export default TermsOfService; diff --git a/src/components/TermsOfService/TermsOfService.module.css b/src/components/TermsOfService/TermsOfService.module.css deleted file mode 100644 index 16e59ac55..000000000 --- a/src/components/TermsOfService/TermsOfService.module.css +++ /dev/null @@ -1,39 +0,0 @@ -@import '../../styles/customMediaQueries.css'; - -.root { - & p { - font-weight: var(--fontWeightMedium); - font-size: 15px; - line-height: 24px; - letter-spacing: 0; - /* margin-top + n * line-height + margin-bottom => x * 6px */ - margin-top: 12px; - margin-bottom: 12px; - - @media (--viewportMedium) { - font-weight: var(--fontWeightMedium); - /* margin-top + n * line-height + margin-bottom => x * 8px */ - margin-top: 17px; - margin-bottom: 15px; - } - } - & h2 { - /* Adjust heading margins to work with the reduced body font size */ - margin: 29px 0 13px 0; - - @media (--viewportMedium) { - margin: 32px 0 0 0; - } - } -} - -.lastUpdated { - composes: marketplaceBodyFontStyles from global; - margin-top: 0; - margin-bottom: 55px; - - @media (--viewportMedium) { - margin-top: 0; - margin-bottom: 54px; - } -} diff --git a/src/components/UserCard/UserCard.example.js b/src/components/UserCard/UserCard.example.js index 857825f9d..e2f7cc482 100644 --- a/src/components/UserCard/UserCard.example.js +++ b/src/components/UserCard/UserCard.example.js @@ -56,13 +56,13 @@ export const WithProfileImageAndBioCurrentUser = { name: 'square-small', width: 240, height: 240, - url: 'https://via.placeholder.com/240x240', + url: 'https://picsum.photos/240/240/', }, 'square-small2x': { name: 'square-small2x', width: 480, height: 480, - url: 'https://via.placeholder.com/480x480', + url: 'https://picsum.photos480/480/', }, }, }, @@ -99,13 +99,13 @@ export const WithProfileImageAndBio = { name: 'square-small', width: 240, height: 240, - url: 'https://via.placeholder.com/240x240', + url: 'https://picsum.photos/240/240/', }, 'square-small2x': { name: 'square-small2x', width: 480, height: 480, - url: 'https://via.placeholder.com/480x480', + url: 'https://picsum.photos/480/480/', }, }, }, diff --git a/src/components/index.js b/src/components/index.js index 31aa651de..85bcf20a8 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -137,7 +137,5 @@ export { default as StripeConnectAccountForm } from './StripeConnectAccountForm/ export { default as LayoutWrapperAccountSettingsSideNav } from './LayoutWrapperAccountSettingsSideNav/LayoutWrapperAccountSettingsSideNav'; export { default as ModalMissingInformation } from './ModalMissingInformation/ModalMissingInformation'; -export { default as PrivacyPolicy } from './PrivacyPolicy/PrivacyPolicy'; -export { default as TermsOfService } from './TermsOfService/TermsOfService'; export { default as Footer } from './Footer/Footer'; export { default as Topbar } from './Topbar/Topbar'; diff --git a/src/containers/AboutPage/AboutPage.js b/src/containers/AboutPage/AboutPage.js deleted file mode 100644 index cdd733018..000000000 --- a/src/containers/AboutPage/AboutPage.js +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; - -import config from '../../config'; -import { twitterPageURL } from '../../util/urlHelpers'; -import { - LayoutSingleColumn, - LayoutWrapperTopbar, - LayoutWrapperMain, - LayoutWrapperFooter, - Footer, - ExternalLink, -} from '../../components'; -import StaticPage from '../../containers/StaticPage/StaticPage'; -import TopbarContainer from '../../containers/TopbarContainer/TopbarContainer'; - -import css from './AboutPage.module.css'; -import image from './about-us-1056.jpg'; - -const AboutPage = () => { - const { siteTwitterHandle, siteFacebookPage } = config; - const siteTwitterPage = twitterPageURL(siteTwitterHandle); - - // prettier-ignore - return ( - - - - - - - -

    There's no such thing as too many sneakers.

    - My first ice cream. - -
    -
    -

    "We've built Sneakertime because we didn't trust anonymous sellers online without recommendations."

    -
    - -
    -

    - The world of Sneakers couldn't be more exciting! Whether you are a casual buyer or an experienced collector, you can find the right pair on Sneakertime and trust sellers that your new favorite item will be swiftly and safely sent to you or ready for pickup. -

    - -

    - Buying sneakers can be stressful: you can find many online websites where to buy them but most don't deliver the trust you can legitimately expect. With Sneakertime, we want to make sure you're transaction will go smoothly: from browsing and checking the stock, to making the order and payment, to the review of the sellers. And we hope you'll be so convinced that you'll soon start selling your least favorite pairs to make new buyers happy! -

    - -

    Do you have sneakers to sell?

    - -

    - Sneakertime offers you a good way to earn some extra cash! If you're not using your - sneakers anymore, why not sell them to other sneakers fans? And if you already have laid your eyes on the pair you want next, selling something from your collection is a great way to get money for your next buy and make room in your closets! -

    - -

    - Create your own marketplace like Sneakertime -

    -

    - Sneakertime is brought to you by the good folks at{' '} - Sharetribe. Would you like - to create your own marketplace platform a bit like Sneakertime? Or perhaps a mobile - app? With Sharetribe it's really easy. If you have a marketplace idea in mind, do - get in touch! -

    -

    - You can also checkout our{' '} - Facebook and{' '} - Twitter. -

    -
    -
    -
    - - -