diff --git a/.circleci/config.yml b/.circleci/config.yml index 5b0ff4a40b..fa18367f07 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -253,6 +253,7 @@ workflows: branches: only: - develop + - feature-contentful # This is stage env for production QA releases - "build-prod-staging": context : org-global diff --git a/src/shared/components/Contentful/Tabs/Tabs.jsx b/src/shared/components/Contentful/Tabs/Tabs.jsx index 57212632ea..f107392203 100644 --- a/src/shared/components/Contentful/Tabs/Tabs.jsx +++ b/src/shared/components/Contentful/Tabs/Tabs.jsx @@ -17,6 +17,7 @@ import { TabPanel, } from 'react-tabs'; import { fixStyle } from 'utils/contentful'; +import { getQuery, updateQuery } from 'utils/url'; import defaultTheme from './themes/style.scss'; import zurichTheme from './themes/zurich.scss'; import tabsGroup from './themes/tabsGroup.scss'; @@ -40,7 +41,38 @@ export const TAB_THEMES = { export default class TabsItemsLoader extends Component { constructor(props) { super(props); - this.state = { tabIndex: props.selected }; + this.state = { + tabIndex: props.selected || 0, + }; + + this.updatePageUrl.bind(this); + } + + componentDidMount() { + const q = getQuery(); + const { tabId } = this.props; + const { tabIndex } = this.state; + if (q.tabs && q.tabs[tabId] && Number(q.tabs[tabId]) !== tabIndex) { + this.setState({ tabIndex: Number(q.tabs[tabId]) }); + } else { + this.updatePageUrl(); + } + } + + componentDidUpdate() { + this.updatePageUrl(); + } + + updatePageUrl() { + const q = getQuery(); + const { tabId } = this.props; + const { tabIndex } = this.state; + updateQuery({ + tabs: { + ...q.tabs, + [tabId]: tabIndex || 0, + }, + }); } render() { @@ -50,6 +82,7 @@ export default class TabsItemsLoader extends Component { spaceName, environment, theme, + tabId, } = this.props; const { tabIndex } = this.state; @@ -129,6 +162,7 @@ export default class TabsItemsLoader extends Component { environment={environment} selected={fields.selected} theme={TAB_THEMES[fields.theme || 'Default']} + tabId={tabId} /> ); } @@ -173,4 +207,5 @@ TabsItemsLoader.propTypes = { environment: PT.string, selected: PT.number, theme: PT.shape().isRequired, + tabId: PT.string.isRequired, }; diff --git a/src/shared/components/Contentful/Tabs/index.jsx b/src/shared/components/Contentful/Tabs/index.jsx index a109794431..cda003fb7f 100644 --- a/src/shared/components/Contentful/Tabs/index.jsx +++ b/src/shared/components/Contentful/Tabs/index.jsx @@ -34,6 +34,7 @@ function ContentfulTabs(props) { environment={environment} selected={fields.selected} theme={TAB_THEMES[fields.theme || 'Default']} + tabId={fields.urlQueryName || id} /> ); }} diff --git a/src/shared/components/Contentful/Viewport/Viewport.jsx b/src/shared/components/Contentful/Viewport/Viewport.jsx index 1b7609d55b..606091089a 100644 --- a/src/shared/components/Contentful/Viewport/Viewport.jsx +++ b/src/shared/components/Contentful/Viewport/Viewport.jsx @@ -4,6 +4,8 @@ import PT from 'prop-types'; import React from 'react'; import { themr } from 'react-css-super-themr'; +import { getHash } from 'utils/url'; + /* Loads viewport content assets. */ const Viewport = ({ @@ -13,6 +15,15 @@ const Viewport = ({ viewportId, animation, }) => { + const hash = getHash(); + if (hash && hash.vid && hash.vid === viewportId) { + setTimeout(() => { + requestAnimationFrame(() => { + const section = document.getElementById(viewportId); + if (section) section.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + }); + } // Animated on scroll viewport? if (animation.animateOnScroll) { return ( diff --git a/src/shared/utils/url.js b/src/shared/utils/url.js index 73154aa74a..dc2a90d0ac 100644 --- a/src/shared/utils/url.js +++ b/src/shared/utils/url.js @@ -8,6 +8,22 @@ import _ from 'lodash'; import qs from 'qs'; import { isomorphy } from 'topcoder-react-utils'; +/** + * Get current URL hash parameters as object + */ +export function getHash() { + if (isomorphy.isServerSide()) return null; + return qs.parse(window.location.hash.slice(1)); +} + +/** + * Get current URL query parameters as object + */ +export function getQuery() { + if (isomorphy.isServerSide()) return {}; + return qs.parse(window.location.search.slice(1)); +} + /** * If executed client-side (determined in this case by the presence of global * window object), this function updates query section of URL; otherwise does @@ -22,6 +38,7 @@ export function updateQuery(update) { if (isomorphy.isServerSide()) return; let query = qs.parse(window.location.search.slice(1)); + const { hash } = window.location; /* _.merge won't work here, because it just ignores the fields explicitely * set as undefined in the objects to be merged, rather than deleting such @@ -31,6 +48,9 @@ export function updateQuery(update) { else query[key] = value; }); query = `?${qs.stringify(query, { encodeValuesOnly: true })}`; + if (hash) { + query += hash; + } window.history.replaceState(window.history.state, '', query); }