diff --git a/webapi/ClientApp/src/App.tsx b/webapi/ClientApp/src/App.tsx index 327d5f4..3cabb5a 100644 --- a/webapi/ClientApp/src/App.tsx +++ b/webapi/ClientApp/src/App.tsx @@ -12,7 +12,7 @@ import { DynamicPage } from './pages' import { RouteModel } from './models' import { ApplicationState } from './store' import { Loader } from './components/Loader' - +import { Helmet } from './components/ReactHelmet' interface IRouteProp { path: string, @@ -44,6 +44,11 @@ const App = () => { const { content, loader } = useSelector((state: ApplicationState) => state) + + const { + siteName = "" + } = content ? content : {} + useEffect(() => { dispatch(settingsActionCreators.requestContent()) }, []) @@ -56,6 +61,15 @@ const App = () => { }, [pathname]) return <> + + {siteName} + + + + + + + {content?.routes ? NestedRoutes(content.routes, 'PublicLayout') : ''} {content?.adminRoutes ? NestedRoutes(content.adminRoutes, 'AdminLayout') : ''} diff --git a/webapi/ClientApp/src/components/ReactFastCompare/LICENSE b/webapi/ClientApp/src/components/ReactFastCompare/LICENSE new file mode 100644 index 0000000..beb3eb8 --- /dev/null +++ b/webapi/ClientApp/src/components/ReactFastCompare/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2018 Formidable Labs +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/webapi/ClientApp/src/components/ReactFastCompare/README.md b/webapi/ClientApp/src/components/ReactFastCompare/README.md new file mode 100644 index 0000000..ae8905f --- /dev/null +++ b/webapi/ClientApp/src/components/ReactFastCompare/README.md @@ -0,0 +1,165 @@ +# react-fast-compare + +[![Downloads][downloads_img]][npm_site] +[![Bundle Size][bundle_img]](#bundle-size) +[![Travis Status][trav_img]][trav_site] +[![AppVeyor Status][appveyor_img]][appveyor_site] +[![npm version][npm_img]][npm_site] +[![Maintenance Status][maintenance_img]](#maintenance-status) + +The fastest deep equal comparison for React. Very quick general-purpose deep +comparison, too. Great for `React.memo` and `shouldComponentUpdate`. + +This is a fork of the brilliant +[fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal) with some +extra handling for React. + +![benchmark chart](assets/benchmarking.png "benchmarking chart") + +(Check out the [benchmarking details](#benchmarking-this-library).) + +## Install + +```sh +$ yarn add react-fast-compare +# or +$ npm install react-fast-compare +``` + +## Highlights + +- ES5 compatible; works in node.js (0.10+) and browsers (IE9+) +- deeply compares any value (besides objects with circular references) +- handles React-specific circular references, like elements +- checks equality Date and RegExp objects +- should as fast as [fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal) via a single unified library, and with added guardrails for circular references. +- small: under 650 bytes minified+gzipped + +## Usage + +```jsx +const isEqual = require("react-fast-compare"); + +// general usage +console.log(isEqual({ foo: "bar" }, { foo: "bar" })); // true + +// React.memo +// only re-render ExpensiveComponent when the props have deeply changed +const DeepMemoComponent = React.memo(ExpensiveComponent, isEqual); + +// React.Component shouldComponentUpdate +// only re-render AnotherExpensiveComponent when the props have deeply changed +class AnotherExpensiveComponent extends React.Component { + shouldComponentUpdate(nextProps) { + return !isEqual(this.props, nextProps); + } + render() { + // ... + } +} +``` + +## Do I Need `React.memo` (or `shouldComponentUpdate`)? + +> What's faster than a really fast deep comparison? No deep comparison at all. + +—This Readme + +Deep checks in `React.memo` or a `shouldComponentUpdate` should not be used blindly. +First, see if the default +[React.memo](https://reactjs.org/docs/react-api.html#reactmemo) or +[PureComponent](https://reactjs.org/docs/react-api.html#reactpurecomponent) +will work for you. If it won't (if you need deep checks), it's wise to make +sure you've correctly indentified the bottleneck in your application by +[profiling the performance](https://reactjs.org/docs/optimizing-performance.html#profiling-components-with-the-chrome-performance-tab). +After you've determined that you _do_ need deep equality checks and you've +identified the minimum number of places to apply them, then this library may +be for you! + +## Benchmarking this Library + +The absolute values are much less important than the relative differences +between packages. + +Benchmarking source can be found +[here](https://github.com/FormidableLabs/react-fast-compare/blob/master/benchmark/index.js). +Each "operation" consists of running all relevant tests. The React benchmark +uses both the generic tests and the react tests; these runs will be slower +simply because there are more tests in each operation. + +The results below are from a local test on a laptop. + +### Generic Data + +``` +react-fast-compare x 157,863 ops/sec ±0.54% (94 runs sampled) +fast-deep-equal x 149,877 ops/sec ±0.76% (93 runs sampled) +lodash.isEqual x 33,298 ops/sec ±0.70% (93 runs sampled) +nano-equal x 144,836 ops/sec ±0.51% (94 runs sampled) +shallow-equal-fuzzy x 110,192 ops/sec ±0.57% (95 runs sampled) + fastest: react-fast-compare +``` + +`react-fast-compare` and `fast-deep-equal` should be the same speed for these +tests; any difference is just noise. `react-fast-compare` won't be faster than +`fast-deep-equal`, because it's based on it. + +### React and Generic Data + +``` +react-fast-compare x 64,102 ops/sec ±0.36% (94 runs sampled) +fast-deep-equal x 63,844 ops/sec ±0.43% (94 runs sampled) +lodash.isEqual x 6,243 ops/sec ±0.72% (90 runs sampled) + fastest: react-fast-compare,fast-deep-equal +``` + +Two of these packages cannot handle comparing React elements, because they +contain circular reference: `nano-equal` and `shallow-equal-fuzzy`. + +### Running Benchmarks + +```sh +$ yarn install +$ yarn run benchmark +``` + +## Differences between this library and `fast-deep-equal` + +`react-fast-compare` is based on `fast-deep-equal`, with some additions: + +- `react-fast-compare` has `try`/`catch` guardrails for stack overflows from undetected (non-React) circular references. +- `react-fast-compare` has a _single_ unified entry point for all uses. No matter what your target application is, `import equal from 'react-fast-compare'` just works. `fast-deep-equal` has multiple entry points for different use cases. + +This version of `react-fast-compare` tracks `fast-deep-equal@3.1.1`. + +## Bundle Size + +There are a variety of ways to calculate bundle size for JavaScript code. +You can see our size test code in the `compress` script in +[`package.json`](https://github.com/FormidableLabs/react-fast-compare/blob/master/package.json). +[Bundlephobia's calculation](https://bundlephobia.com/result?p=react-fast-compare) is slightly higher, +as they [do not mangle during minification](https://github.com/pastelsky/package-build-stats/blob/v6.1.1/src/getDependencySizeTree.js#L139). + +## License + +[MIT](https://github.com/FormidableLabs/react-fast-compare/blob/readme/LICENSE) + +## Contributing + +Please see our [contributions guide](./CONTRIBUTING.md). + +## Maintenance Status + +**Active:** Formidable is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome. + +[trav_img]: https://api.travis-ci.com/FormidableLabs/react-fast-compare.svg +[trav_site]: https://travis-ci.com/FormidableLabs/react-fast-compare +[cov_img]: https://img.shields.io/coveralls/FormidableLabs/react-fast-compare.svg +[cov_site]: https://coveralls.io/r/FormidableLabs/react-fast-compare +[npm_img]: https://badge.fury.io/js/react-fast-compare.svg +[npm_site]: http://badge.fury.io/js/react-fast-compare +[appveyor_img]: https://ci.appveyor.com/api/projects/status/github/formidablelabs/react-fast-compare?branch=master&svg=true +[appveyor_site]: https://ci.appveyor.com/project/FormidableLabs/react-fast-compare +[bundle_img]: https://img.shields.io/badge/minzipped%20size-622%20B-flatgreen.svg +[downloads_img]: https://img.shields.io/npm/dm/react-fast-compare.svg +[maintenance_img]: https://img.shields.io/badge/maintenance-active-flatgreen.svg \ No newline at end of file diff --git a/webapi/ClientApp/src/components/ReactFastCompare/index.js b/webapi/ClientApp/src/components/ReactFastCompare/index.js new file mode 100644 index 0000000..a4d683e --- /dev/null +++ b/webapi/ClientApp/src/components/ReactFastCompare/index.js @@ -0,0 +1,126 @@ +/* global Map:readonly, Set:readonly, ArrayBuffer:readonly */ + +var hasElementType = typeof Element !== 'undefined' +var hasMap = typeof Map === 'function' +var hasSet = typeof Set === 'function' +var hasArrayBuffer = typeof ArrayBuffer === 'function' + +// Note: We **don't** need `envHasBigInt64Array` in fde es6/index.js + +const equal = (a, b) => { + // START: fast-deep-equal es6/index.js 3.1.1 + if (a === b) return true + + if (a && b && typeof a === 'object' && typeof b === 'object') { + if (a.constructor !== b.constructor) return false + + var length, i, keys + if (Array.isArray(a)) { + length = a.length + if (length !== b.length) return false + for (i = length; i-- !== 0;) { if (!equal(a[i], b[i])) return false } + return true + } + + // START: Modifications: + // 1. Extra `has &&` helpers in initial condition allow es6 code + // to co-exist with es5. + // 2. Replace `for of` with es5 compliant iteration using `for`. + // Basically, take: + // + // ```js + // for (i of a.entries()) + // if (!b.has(i[0])) return false; + // ``` + // + // ... and convert to: + // + // ```js + // it = a.entries(); + // while (!(i = it.next()).done) + // if (!b.has(i.value[0])) return false; + // ``` + // + // **Note**: `i` access switches to `i.value`. + var it + if (hasMap && (a instanceof Map) && (b instanceof Map)) { + if (a.size !== b.size) return false + it = a.entries() + while (!(i = it.next()).done) { if (!b.has(i.value[0])) return false } + it = a.entries() + while (!(i = it.next()).done) { if (!equal(i.value[1], b.get(i.value[0]))) return false } + return true + } + + if (hasSet && (a instanceof Set) && (b instanceof Set)) { + if (a.size !== b.size) return false + it = a.entries() + while (!(i = it.next()).done) { if (!b.has(i.value[0])) return false } + return true + } + // END: Modifications + + if (hasArrayBuffer && ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { + length = a.length + if (length != b.length) return false + for (i = length; i-- !== 0;) { if (a[i] !== b[i]) return false } + return true + } + + if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags + if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf() + if (a.toString !== Object.prototype.toString) return a.toString() === b.toString() + + keys = Object.keys(a) + length = keys.length + if (length !== Object.keys(b).length) return false + + for (i = length; i-- !== 0;) { if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false } + // END: fast-deep-equal + + // START: react-fast-compare + // custom handling for DOM elements + if (hasElementType && a instanceof Element) return false + + // custom handling for React + for (i = length; i-- !== 0;) { + if (keys[i] === '_owner' && a.$$typeof) { + // React-specific: avoid traversing React elements' _owner. + // _owner contains circular references + // and is not needed when comparing the actual elements (and not their owners) + // .$$typeof and ._store on just reasonable markers of a react element + continue + } + + // all other properties should be traversed as usual + if (!equal(a[keys[i]], b[keys[i]])) return false + } + // END: react-fast-compare + + // START: fast-deep-equal + return true + } + + return a !== a && b !== b +} +// end fast-deep-equal + +const isEqual = (a, b) => { + try { + return equal(a, b) + } catch (error) { + if (((error.message || '').match(/stack|recursion/i))) { + // warn on circular references, don't crash + // browsers give this different errors name and messages: + // chrome/safari: "RangeError", "Maximum call stack size exceeded" + // firefox: "InternalError", too much recursion" + // edge: "Error", "Out of stack space" + console.warn('react-fast-compare cannot handle circular refs') + return false + } + // some other error. we should definitely know about these + throw error + } +} + +export default isEqual diff --git a/webapi/ClientApp/src/components/ReactHelmet/HelmetConstants.js b/webapi/ClientApp/src/components/ReactHelmet/HelmetConstants.js new file mode 100644 index 0000000..0017f6b --- /dev/null +++ b/webapi/ClientApp/src/components/ReactHelmet/HelmetConstants.js @@ -0,0 +1,68 @@ +export const ATTRIBUTE_NAMES = { + BODY: 'bodyAttributes', + HTML: 'htmlAttributes', + TITLE: 'titleAttributes' +} + +export const TAG_NAMES = { + BASE: 'base', + BODY: 'body', + HEAD: 'head', + HTML: 'html', + LINK: 'link', + META: 'meta', + NOSCRIPT: 'noscript', + SCRIPT: 'script', + STYLE: 'style', + TITLE: 'title' +} + +export const VALID_TAG_NAMES = Object.keys(TAG_NAMES).map( + name => TAG_NAMES[name] +) + +export const TAG_PROPERTIES = { + CHARSET: 'charset', + CSS_TEXT: 'cssText', + HREF: 'href', + HTTPEQUIV: 'http-equiv', + INNER_HTML: 'innerHTML', + ITEM_PROP: 'itemprop', + NAME: 'name', + PROPERTY: 'property', + REL: 'rel', + SRC: 'src', + TARGET: 'target' +} + +export const REACT_TAG_MAP = { + accesskey: 'accessKey', + charset: 'charSet', + class: 'className', + contenteditable: 'contentEditable', + contextmenu: 'contextMenu', + 'http-equiv': 'httpEquiv', + itemprop: 'itemProp', + tabindex: 'tabIndex' +} + +export const HELMET_PROPS = { + DEFAULT_TITLE: 'defaultTitle', + DEFER: 'defer', + ENCODE_SPECIAL_CHARACTERS: 'encodeSpecialCharacters', + ON_CHANGE_CLIENT_STATE: 'onChangeClientState', + TITLE_TEMPLATE: 'titleTemplate' +} + +export const HTML_TAG_MAP = Object.keys(REACT_TAG_MAP).reduce((obj, key) => { + obj[REACT_TAG_MAP[key]] = key + return obj +}, {}) + +export const SELF_CLOSING_TAGS = [ + TAG_NAMES.NOSCRIPT, + TAG_NAMES.SCRIPT, + TAG_NAMES.STYLE +] + +export const HELMET_ATTRIBUTE = 'data-react-helmet' diff --git a/webapi/ClientApp/src/components/ReactHelmet/HelmetUtils.js b/webapi/ClientApp/src/components/ReactHelmet/HelmetUtils.js new file mode 100644 index 0000000..40f2364 --- /dev/null +++ b/webapi/ClientApp/src/components/ReactHelmet/HelmetUtils.js @@ -0,0 +1,657 @@ +import React from 'react' + +import { + ATTRIBUTE_NAMES, + HELMET_ATTRIBUTE, + HELMET_PROPS, + HTML_TAG_MAP, + REACT_TAG_MAP, + SELF_CLOSING_TAGS, + TAG_NAMES, + TAG_PROPERTIES +} from './HelmetConstants' + +const encodeSpecialCharacters = (str, encode = true) => { + if (encode === false) { + return String(str) + } + + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +const getTitleFromPropsList = propsList => { + const innermostTitle = getInnermostProperty(propsList, TAG_NAMES.TITLE) + const innermostTemplate = getInnermostProperty( + propsList, + HELMET_PROPS.TITLE_TEMPLATE + ) + + if (innermostTemplate && innermostTitle) { + // use function arg to avoid need to escape $ characters + return innermostTemplate.replace( + /%s/g, + () => + Array.isArray(innermostTitle) + ? innermostTitle.join('') + : innermostTitle + ) + } + + const innermostDefaultTitle = getInnermostProperty( + propsList, + HELMET_PROPS.DEFAULT_TITLE + ) + + return innermostTitle || innermostDefaultTitle || undefined +} + +const getOnChangeClientState = propsList => { + return ( + getInnermostProperty(propsList, HELMET_PROPS.ON_CHANGE_CLIENT_STATE) || + (() => {}) + ) +} + +const getAttributesFromPropsList = (tagType, propsList) => { + return propsList + .filter(props => typeof props[tagType] !== 'undefined') + .map(props => props[tagType]) + .reduce((tagAttrs, current) => { + return { ...tagAttrs, ...current } + }, {}) +} + +const getBaseTagFromPropsList = (primaryAttributes, propsList) => { + return propsList + .filter(props => typeof props[TAG_NAMES.BASE] !== 'undefined') + .map(props => props[TAG_NAMES.BASE]) + .reverse() + .reduce((innermostBaseTag, tag) => { + if (!innermostBaseTag.length) { + const keys = Object.keys(tag) + + for (let i = 0; i < keys.length; i++) { + const attributeKey = keys[i] + const lowerCaseAttributeKey = attributeKey.toLowerCase() + + if ( + primaryAttributes.indexOf(lowerCaseAttributeKey) !== + -1 && + tag[lowerCaseAttributeKey] + ) { + return innermostBaseTag.concat(tag) + } + } + } + + return innermostBaseTag + }, []) +} + +const getTagsFromPropsList = (tagName, primaryAttributes, propsList) => { +// Calculate list of tags, giving priority innermost component (end of the propslist) + const approvedSeenTags = {} + + return propsList + .filter(props => { + if (Array.isArray(props[tagName])) { + return true + } + if (typeof props[tagName] !== 'undefined') { + warn( + `Helmet: ${tagName} should be of type "Array". Instead found type "${typeof props[ + tagName + ]}"` + ) + } + return false + }) + .map(props => props[tagName]) + .reverse() + .reduce((approvedTags, instanceTags) => { + const instanceSeenTags = {} + + instanceTags + .filter(tag => { + let primaryAttributeKey + const keys = Object.keys(tag) + for (let i = 0; i < keys.length; i++) { + const attributeKey = keys[i] + const lowerCaseAttributeKey = attributeKey.toLowerCase() + + // Special rule with link tags, since rel and href are both primary tags, rel takes priority + if ( + primaryAttributes.indexOf(lowerCaseAttributeKey) !== + -1 && + !( + primaryAttributeKey === TAG_PROPERTIES.REL && + tag[primaryAttributeKey].toLowerCase() === + 'canonical' + ) && + !( + lowerCaseAttributeKey === TAG_PROPERTIES.REL && + tag[lowerCaseAttributeKey].toLowerCase() === + 'stylesheet' + ) + ) { + primaryAttributeKey = lowerCaseAttributeKey + } + // Special case for innerHTML which doesn't work lowercased + if ( + primaryAttributes.indexOf(attributeKey) !== -1 && + (attributeKey === TAG_PROPERTIES.INNER_HTML || + attributeKey === TAG_PROPERTIES.CSS_TEXT || + attributeKey === TAG_PROPERTIES.ITEM_PROP) + ) { + primaryAttributeKey = attributeKey + } + } + + if (!primaryAttributeKey || !tag[primaryAttributeKey]) { + return false + } + + const value = tag[primaryAttributeKey].toLowerCase() + + if (!approvedSeenTags[primaryAttributeKey]) { + approvedSeenTags[primaryAttributeKey] = {} + } + + if (!instanceSeenTags[primaryAttributeKey]) { + instanceSeenTags[primaryAttributeKey] = {} + } + + if (!approvedSeenTags[primaryAttributeKey][value]) { + instanceSeenTags[primaryAttributeKey][value] = true + return true + } + + return false + }) + .reverse() + .forEach(tag => approvedTags.push(tag)) + + // Update seen tags with tags from this instance + const keys = Object.keys(instanceSeenTags) + for (let i = 0; i < keys.length; i++) { + const attributeKey = keys[i] + const tagUnion = Object.assign( + {}, + approvedSeenTags[attributeKey], + instanceSeenTags[attributeKey] + ) + + approvedSeenTags[attributeKey] = tagUnion + } + + return approvedTags + }, []) + .reverse() +} + +const getInnermostProperty = (propsList, property) => { + for (let i = propsList.length - 1; i >= 0; i--) { + const props = propsList[i] + + if (props.hasOwnProperty(property)) { + return props[property] + } + } + + return null +} + +const reducePropsToState = propsList => ({ + baseTag: getBaseTagFromPropsList( + [TAG_PROPERTIES.HREF, TAG_PROPERTIES.TARGET], + propsList + ), + bodyAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.BODY, propsList), + defer: getInnermostProperty(propsList, HELMET_PROPS.DEFER), + encode: getInnermostProperty( + propsList, + HELMET_PROPS.ENCODE_SPECIAL_CHARACTERS + ), + htmlAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.HTML, propsList), + linkTags: getTagsFromPropsList( + TAG_NAMES.LINK, + [TAG_PROPERTIES.REL, TAG_PROPERTIES.HREF], + propsList + ), + metaTags: getTagsFromPropsList( + TAG_NAMES.META, + [ + TAG_PROPERTIES.NAME, + TAG_PROPERTIES.CHARSET, + TAG_PROPERTIES.HTTPEQUIV, + TAG_PROPERTIES.PROPERTY, + TAG_PROPERTIES.ITEM_PROP + ], + propsList + ), + noscriptTags: getTagsFromPropsList( + TAG_NAMES.NOSCRIPT, + [TAG_PROPERTIES.INNER_HTML], + propsList + ), + onChangeClientState: getOnChangeClientState(propsList), + scriptTags: getTagsFromPropsList( + TAG_NAMES.SCRIPT, + [TAG_PROPERTIES.SRC, TAG_PROPERTIES.INNER_HTML], + propsList + ), + styleTags: getTagsFromPropsList( + TAG_NAMES.STYLE, + [TAG_PROPERTIES.CSS_TEXT], + propsList + ), + title: getTitleFromPropsList(propsList), + titleAttributes: getAttributesFromPropsList( + ATTRIBUTE_NAMES.TITLE, + propsList + ) +}) + +const rafPolyfill = (() => { + let clock = Date.now() + + return (callback) => { + const currentTime = Date.now() + + if (currentTime - clock > 16) { + clock = currentTime + callback(currentTime) + } else { + setTimeout(() => { + rafPolyfill(callback) + }, 0) + } + } +})() + +const cafPolyfill = (id) => clearTimeout(id) + +const requestAnimationFrame = +typeof window !== 'undefined' + ? (window.requestAnimationFrame && + window.requestAnimationFrame.bind(window)) || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + rafPolyfill + : global.requestAnimationFrame || rafPolyfill + +const cancelAnimationFrame = +typeof window !== 'undefined' + ? window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + cafPolyfill + : global.cancelAnimationFrame || cafPolyfill + +const warn = msg => { + return console && typeof console.warn === 'function' && console.warn(msg) +} + +let _helmetCallback = null + +const handleClientStateChange = newState => { + if (_helmetCallback) { + cancelAnimationFrame(_helmetCallback) + } + + if (newState.defer) { + _helmetCallback = requestAnimationFrame(() => { + commitTagChanges(newState, () => { + _helmetCallback = null + }) + }) + } else { + commitTagChanges(newState) + _helmetCallback = null + } +} + +const commitTagChanges = (newState, cb) => { + const { + baseTag, + bodyAttributes, + htmlAttributes, + linkTags, + metaTags, + noscriptTags, + onChangeClientState, + scriptTags, + styleTags, + title, + titleAttributes + } = newState + updateAttributes(TAG_NAMES.BODY, bodyAttributes) + updateAttributes(TAG_NAMES.HTML, htmlAttributes) + + updateTitle(title, titleAttributes) + + const tagUpdates = { + baseTag: updateTags(TAG_NAMES.BASE, baseTag), + linkTags: updateTags(TAG_NAMES.LINK, linkTags), + metaTags: updateTags(TAG_NAMES.META, metaTags), + noscriptTags: updateTags(TAG_NAMES.NOSCRIPT, noscriptTags), + scriptTags: updateTags(TAG_NAMES.SCRIPT, scriptTags), + styleTags: updateTags(TAG_NAMES.STYLE, styleTags) + } + + const addedTags = {} + const removedTags = {} + + Object.keys(tagUpdates).forEach(tagType => { + const { newTags, oldTags } = tagUpdates[tagType] + + if (newTags.length) { + addedTags[tagType] = newTags + } + if (oldTags.length) { + removedTags[tagType] = tagUpdates[tagType].oldTags + } + }) + + cb && cb() + + onChangeClientState(newState, addedTags, removedTags) +} + +const flattenArray = possibleArray => { + return Array.isArray(possibleArray) + ? possibleArray.join('') + : possibleArray +} + +const updateTitle = (title, attributes) => { + if (typeof title !== 'undefined' && document.title !== title) { + document.title = flattenArray(title) + } + + updateAttributes(TAG_NAMES.TITLE, attributes) +} + +const updateAttributes = (tagName, attributes) => { + const elementTag = document.getElementsByTagName(tagName)[0] + + if (!elementTag) { + return + } + + const helmetAttributeString = elementTag.getAttribute(HELMET_ATTRIBUTE) + const helmetAttributes = helmetAttributeString + ? helmetAttributeString.split(',') + : [] + const attributesToRemove = [].concat(helmetAttributes) + const attributeKeys = Object.keys(attributes) + + for (let i = 0; i < attributeKeys.length; i++) { + const attribute = attributeKeys[i] + const value = attributes[attribute] || '' + + if (elementTag.getAttribute(attribute) !== value) { + elementTag.setAttribute(attribute, value) + } + + if (helmetAttributes.indexOf(attribute) === -1) { + helmetAttributes.push(attribute) + } + + const indexToSave = attributesToRemove.indexOf(attribute) + if (indexToSave !== -1) { + attributesToRemove.splice(indexToSave, 1) + } + } + + for (let i = attributesToRemove.length - 1; i >= 0; i--) { + elementTag.removeAttribute(attributesToRemove[i]) + } + + if (helmetAttributes.length === attributesToRemove.length) { + elementTag.removeAttribute(HELMET_ATTRIBUTE) + } else if ( + elementTag.getAttribute(HELMET_ATTRIBUTE) !== attributeKeys.join(',') + ) { + elementTag.setAttribute(HELMET_ATTRIBUTE, attributeKeys.join(',')) + } +} + +const updateTags = (type, tags) => { + const headElement = document.head || document.querySelector(TAG_NAMES.HEAD) + const tagNodes = headElement.querySelectorAll( + `${type}[${HELMET_ATTRIBUTE}]` + ) + const oldTags = Array.prototype.slice.call(tagNodes) + const newTags = [] + let indexToDelete + + if (tags && tags.length) { + tags.forEach(tag => { + const newElement = document.createElement(type) + + for (const attribute in tag) { + if (tag.hasOwnProperty(attribute)) { + if (attribute === TAG_PROPERTIES.INNER_HTML) { + newElement.innerHTML = tag.innerHTML + } else if (attribute === TAG_PROPERTIES.CSS_TEXT) { + if (newElement.styleSheet) { + newElement.styleSheet.cssText = tag.cssText + } else { + newElement.appendChild( + document.createTextNode(tag.cssText) + ) + } + } else { + const value = + typeof tag[attribute] === 'undefined' + ? '' + : tag[attribute] + newElement.setAttribute(attribute, value) + } + } + } + + newElement.setAttribute(HELMET_ATTRIBUTE, 'true') + + // Remove a duplicate tag from domTagstoRemove, so it isn't cleared. + if ( + oldTags.some((existingTag, index) => { + indexToDelete = index + return newElement.isEqualNode(existingTag) + }) + ) { + oldTags.splice(indexToDelete, 1) + } else { + newTags.push(newElement) + } + }) + } + + oldTags.forEach(tag => tag.parentNode.removeChild(tag)) + newTags.forEach(tag => headElement.appendChild(tag)) + + return { + oldTags, + newTags + } +} + +const generateElementAttributesAsString = attributes => + Object.keys(attributes).reduce((str, key) => { + const attr = + typeof attributes[key] !== 'undefined' + ? `${key}="${attributes[key]}"` + : `${key}` + return str ? `${str} ${attr}` : attr + }, '') + +const generateTitleAsString = (type, title, attributes, encode) => { + const attributeString = generateElementAttributesAsString(attributes) + const flattenedTitle = flattenArray(title) + return attributeString + ? `<${type} ${HELMET_ATTRIBUTE}="true" ${attributeString}>${encodeSpecialCharacters( + flattenedTitle, + encode + )}` + : `<${type} ${HELMET_ATTRIBUTE}="true">${encodeSpecialCharacters( + flattenedTitle, + encode + )}` +} + +const generateTagsAsString = (type, tags, encode) => + tags.reduce((str, tag) => { + const attributeHtml = Object.keys(tag) + .filter( + attribute => + !( + attribute === TAG_PROPERTIES.INNER_HTML || + attribute === TAG_PROPERTIES.CSS_TEXT + ) + ) + .reduce((string, attribute) => { + const attr = + typeof tag[attribute] === 'undefined' + ? attribute + : `${attribute}="${encodeSpecialCharacters( + tag[attribute], + encode + )}"` + return string ? `${string} ${attr}` : attr + }, '') + + const tagContent = tag.innerHTML || tag.cssText || '' + + const isSelfClosing = SELF_CLOSING_TAGS.indexOf(type) === -1 + + return `${str}<${type} ${HELMET_ATTRIBUTE}="true" ${attributeHtml}${ + isSelfClosing ? '/>' : `>${tagContent}` + }` + }, '') + +const convertElementAttributestoReactProps = (attributes, initProps = {}) => { + return Object.keys(attributes).reduce((obj, key) => { + obj[REACT_TAG_MAP[key] || key] = attributes[key] + return obj + }, initProps) +} + +const convertReactPropstoHtmlAttributes = (props, initAttributes = {}) => { + return Object.keys(props).reduce((obj, key) => { + obj[HTML_TAG_MAP[key] || key] = props[key] + return obj + }, initAttributes) +} + +const generateTitleAsReactComponent = (type, title, attributes) => { +// assigning into an array to define toString function on it + const initProps = { + key: title, + [HELMET_ATTRIBUTE]: true + } + const props = convertElementAttributestoReactProps(attributes, initProps) + + return [React.createElement(TAG_NAMES.TITLE, props, title)] +} + +const generateTagsAsReactComponent = (type, tags) => + tags.map((tag, i) => { + const mappedTag = { + key: i, + [HELMET_ATTRIBUTE]: true + } + + Object.keys(tag).forEach(attribute => { + const mappedAttribute = REACT_TAG_MAP[attribute] || attribute + + if ( + mappedAttribute === TAG_PROPERTIES.INNER_HTML || + mappedAttribute === TAG_PROPERTIES.CSS_TEXT + ) { + const content = tag.innerHTML || tag.cssText + mappedTag.dangerouslySetInnerHTML = { __html: content } + } else { + mappedTag[mappedAttribute] = tag[attribute] + } + }) + + return React.createElement(type, mappedTag) + }) + +const getMethodsForTag = (type, tags, encode) => { + switch (type) { + case TAG_NAMES.TITLE: + return { + toComponent: () => + generateTitleAsReactComponent( + type, + tags.title, + tags.titleAttributes, + encode + ), + toString: () => + generateTitleAsString( + type, + tags.title, + tags.titleAttributes, + encode + ) + } + case ATTRIBUTE_NAMES.BODY: + case ATTRIBUTE_NAMES.HTML: + return { + toComponent: () => convertElementAttributestoReactProps(tags), + toString: () => generateElementAttributesAsString(tags) + } + default: + return { + toComponent: () => generateTagsAsReactComponent(type, tags), + toString: () => generateTagsAsString(type, tags, encode) + } + } +} + +const mapStateOnServer = ({ + baseTag, + bodyAttributes, + encode, + htmlAttributes, + linkTags, + metaTags, + noscriptTags, + scriptTags, + styleTags, + title = '', + titleAttributes +}) => ({ + base: getMethodsForTag(TAG_NAMES.BASE, baseTag, encode), + bodyAttributes: getMethodsForTag( + ATTRIBUTE_NAMES.BODY, + bodyAttributes, + encode + ), + htmlAttributes: getMethodsForTag( + ATTRIBUTE_NAMES.HTML, + htmlAttributes, + encode + ), + link: getMethodsForTag(TAG_NAMES.LINK, linkTags, encode), + meta: getMethodsForTag(TAG_NAMES.META, metaTags, encode), + noscript: getMethodsForTag(TAG_NAMES.NOSCRIPT, noscriptTags, encode), + script: getMethodsForTag(TAG_NAMES.SCRIPT, scriptTags, encode), + style: getMethodsForTag(TAG_NAMES.STYLE, styleTags, encode), + title: getMethodsForTag(TAG_NAMES.TITLE, { title, titleAttributes }, encode) +}) + +export { convertReactPropstoHtmlAttributes } +export { handleClientStateChange } +export { mapStateOnServer } +export { reducePropsToState } +export { requestAnimationFrame } +export { warn } diff --git a/webapi/ClientApp/src/components/ReactHelmet/LICENSE b/webapi/ClientApp/src/components/ReactHelmet/LICENSE new file mode 100644 index 0000000..6206505 --- /dev/null +++ b/webapi/ClientApp/src/components/ReactHelmet/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 NFL + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/webapi/ClientApp/src/components/ReactHelmet/README.md b/webapi/ClientApp/src/components/ReactHelmet/README.md new file mode 100644 index 0000000..d1955d0 --- /dev/null +++ b/webapi/ClientApp/src/components/ReactHelmet/README.md @@ -0,0 +1,269 @@ + + +# React Helmet + +[![npm Version](https://img.shields.io/npm/v/react-helmet.svg?style=flat-square)](https://www.npmjs.org/package/react-helmet) +[![codecov.io](https://img.shields.io/codecov/c/github/nfl/react-helmet.svg?branch=master&style=flat-square)](https://codecov.io/github/nfl/react-helmet?branch=master) +[![Build Status](https://img.shields.io/travis/nfl/react-helmet/master.svg?style=flat-square)](https://travis-ci.org/nfl/react-helmet) +[![Dependency Status](https://img.shields.io/david/nfl/react-helmet.svg?style=flat-square)](https://david-dm.org/nfl/react-helmet) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](CONTRIBUTING.md#pull-requests) + +This reusable React component will manage all of your changes to the document head. + +Helmet _takes_ plain HTML tags and _outputs_ plain HTML tags. It's dead simple, and React beginner friendly. + +## [6.0.0-beta Release Notes](https://github.com/nfl/react-helmet/wiki/Upgrade-from-5.x.x----6.x.x-beta) + + +## Example +```javascript +import React from "react"; +import {Helmet} from "react-helmet"; + +class Application extends React.Component { + render () { + return ( +
+ + + My Title + + + ... +
+ ); + } +}; +``` + +Nested or latter components will override duplicate changes: + +```javascript + + + My Title + + + + + + Nested Title + + + + +``` + +outputs: + +```html + + Nested Title + + +``` + +See below for a full reference guide. + +## Features +- Supports all valid head tags: `title`, `base`, `meta`, `link`, `script`, `noscript`, and `style` tags. +- Supports attributes for `body`, `html` and `title` tags. +- Supports server-side rendering. +- Nested components override duplicate head changes. +- Duplicate head changes are preserved when specified in the same component (support for tags like "apple-touch-icon"). +- Callback for tracking DOM changes. + +## Compatibility + +Helmet 5 is fully backward-compatible with previous Helmet releases, so you can upgrade at any time without fear of breaking changes. We encourage you to update your code to our more semantic API, but please feel free to do so at your own pace. + +## Installation + +Yarn: +```bash +yarn add react-helmet +``` + +npm: +```bash +npm install --save react-helmet +``` + +## Server Usage +To use on the server, call `Helmet.renderStatic()` after `ReactDOMServer.renderToString` or `ReactDOMServer.renderToStaticMarkup` to get the head data for use in your prerender. + +Because this component keeps track of mounted instances, **you have to make sure to call `renderStatic` on server**, or you'll get a memory leak. + +```javascript +ReactDOMServer.renderToString(); +const helmet = Helmet.renderStatic(); +``` + +This `helmet` instance contains the following properties: +- `base` +- `bodyAttributes` +- `htmlAttributes` +- `link` +- `meta` +- `noscript` +- `script` +- `style` +- `title` + +Each property contains `toComponent()` and `toString()` methods. Use whichever is appropriate for your environment. For attributes, use the JSX spread operator on the object returned by `toComponent()`. E.g: + +### As string output +```javascript +const html = ` + + + + ${helmet.title.toString()} + ${helmet.meta.toString()} + ${helmet.link.toString()} + + +
+ // React stuff here +
+ + +`; +``` + +### As React components +```javascript +function HTML () { + const htmlAttrs = helmet.htmlAttributes.toComponent(); + const bodyAttrs = helmet.bodyAttributes.toComponent(); + + return ( + + + {helmet.title.toComponent()} + {helmet.meta.toComponent()} + {helmet.link.toComponent()} + + +
+ // React stuff here +
+ + + ); +} +``` + +### Note: Use the same instance +If you are using a prebuilt compilation of your app with webpack in the server be sure to include this in the `webpack file` so that the same instance of `react-helmet` is used. +``` +externals: ["react-helmet"], +``` +Or to import the *react-helmet* instance from the app on the server. + +### Reference Guide + +```javascript + + Nested Title + + + outputs: + + + Nested Title | MyAwesomeWebsite.com + + */} + titleTemplate="MySite.com - %s" + + {/* + (optional) used as a fallback when a template exists but a title is not defined + + + + outputs: + + + My Site + + */} + defaultTitle="My Default Title" + + {/* (optional) callback that tracks DOM changes */} + onChangeClientState={(newState, addedTags, removedTags) => console.log(newState, addedTags, removedTags)} +> + {/* html attributes */} + + + {/* body attributes */} + + + {/* title attributes and value */} + My Plain Title or {`dynamic`} title + + {/* base element */} + + + {/* multiple meta elements */} + + + + {/* multiple link elements */} + + + + {locales.map((locale) => { + + })} + + {/* multiple script elements */} + + + {/* noscript elements */} + + + {/* inline style elements */} + + +``` + +## Contributing to this project +Please take a moment to review the [guidelines for contributing](CONTRIBUTING.md). + +* [Pull requests](CONTRIBUTING.md#pull-requests) +* [Development Process](CONTRIBUTING.md#development) + +## License + +MIT + + \ No newline at end of file diff --git a/webapi/ClientApp/src/components/ReactHelmet/Types.ts b/webapi/ClientApp/src/components/ReactHelmet/Types.ts new file mode 100644 index 0000000..931ec3c --- /dev/null +++ b/webapi/ClientApp/src/components/ReactHelmet/Types.ts @@ -0,0 +1,107 @@ +// Type definitions for react-helmet 6.1 +// Project: https://github.com/nfl/react-helmet +// Definitions by: Evan Bremer +// Isman Usoh +// Kok Sam +// Yui T. +// Yamagishi Kazutoshi +// Justin Hall +// Andriy2 +// Piotr Błażejewicz +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +import * as React from "react" + +interface OtherElementAttributes { + [key: string]: string | number | boolean | null | undefined; +} + +type HtmlProps = JSX.IntrinsicElements["html"] & OtherElementAttributes; + +type BodyProps = JSX.IntrinsicElements["body"] & OtherElementAttributes; + +type LinkProps = JSX.IntrinsicElements["link"]; + +type MetaProps = JSX.IntrinsicElements["meta"]; + +export interface HelmetTags { + baseTag: any[]; + linkTags: HTMLLinkElement[]; + metaTags: HTMLMetaElement[]; + noscriptTags: any[]; + scriptTags: HTMLScriptElement[]; + styleTags: HTMLStyleElement[]; +} + +export interface HelmetProps { + async?: boolean | undefined; + base?: any; + bodyAttributes?: BodyProps | undefined; + children?: React.ReactNode; + defaultTitle?: string | undefined; + defer?: boolean | undefined; + encodeSpecialCharacters?: boolean | undefined; + htmlAttributes?: HtmlProps | undefined; + onChangeClientState?: ((newState: any, addedTags: HelmetTags, removedTags: HelmetTags) => void) | undefined; + link?: LinkProps[] | undefined; + meta?: MetaProps[] | undefined; + noscript?: any[] | undefined; + script?: any[] | undefined; + style?: any[] | undefined; + title?: string | undefined; + titleAttributes?: object | undefined; + titleTemplate?: string | undefined; +} + +/** + * Used by Helmet.peek() + */ +export type HelmetPropsToState = HelmetTags & +Pick< +HelmetProps, +"bodyAttributes" | "defer" | "htmlAttributes" | "onChangeClientState" | "title" | "titleAttributes" +> & { + encode: Required; +}; + +declare class Helmet extends React.Component { + static peek(): HelmetPropsToState; + static rewind(): HelmetData; + static renderStatic(): HelmetData; + static canUseDOM: boolean +} + +declare const HelmetExport: typeof Helmet + +export { HelmetExport as Helmet } +export default HelmetExport + +export interface HelmetData { + base: HelmetDatum; + bodyAttributes: HelmetHTMLBodyDatum; + htmlAttributes: HelmetHTMLElementDatum; + link: HelmetDatum; + meta: HelmetDatum; + noscript: HelmetDatum; + script: HelmetDatum; + style: HelmetDatum; + title: HelmetDatum; + titleAttributes: HelmetDatum; +} + +export interface HelmetDatum { + toString(): string; + toComponent(): React.ReactElement; +} + +export interface HelmetHTMLBodyDatum { + toString(): string; + toComponent(): React.HTMLAttributes; +} + +export interface HelmetHTMLElementDatum { + toString(): string; + toComponent(): React.HTMLAttributes; +} + +export let canUseDOM: boolean \ No newline at end of file diff --git a/webapi/ClientApp/src/components/ReactHelmet/index.js b/webapi/ClientApp/src/components/ReactHelmet/index.js new file mode 100644 index 0000000..96655eb --- /dev/null +++ b/webapi/ClientApp/src/components/ReactHelmet/index.js @@ -0,0 +1,295 @@ +import React from "react" +import PropTypes from "prop-types" +import withSideEffect from '../ReactSideEffect' +import isEqual from '../ReactFastCompare' +import { + convertReactPropstoHtmlAttributes, + handleClientStateChange, + mapStateOnServer, + reducePropsToState, + warn +} from "./HelmetUtils.js" +import {TAG_NAMES, VALID_TAG_NAMES} from "./HelmetConstants.js" + +const Helmet = Component => + class HelmetWrapper extends React.Component { + /** + * @param {Object} base: {"target": "_blank", "href": "http://mysite.com/"} + * @param {Object} bodyAttributes: {"className": "root"} + * @param {String} defaultTitle: "Default Title" + * @param {Boolean} defer: true + * @param {Boolean} encodeSpecialCharacters: true + * @param {Object} htmlAttributes: {"lang": "en", "amp": undefined} + * @param {Array} link: [{"rel": "canonical", "href": "http://mysite.com/example"}] + * @param {Array} meta: [{"name": "description", "content": "Test description"}] + * @param {Array} noscript: [{"innerHTML": " console.log(newState)" + * @param {Array} script: [{"type": "text/javascript", "src": "http://mysite.com/js/test.js"}] + * @param {Array} style: [{"type": "text/css", "cssText": "div { display: block; color: blue; }"}] + * @param {String} title: "Title" + * @param {Object} titleAttributes: {"itemprop": "name"} + * @param {String} titleTemplate: "MySite.com - %s" + */ + static propTypes = { + base: PropTypes.object, + bodyAttributes: PropTypes.object, + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node + ]), + defaultTitle: PropTypes.string, + defer: PropTypes.bool, + encodeSpecialCharacters: PropTypes.bool, + htmlAttributes: PropTypes.object, + link: PropTypes.arrayOf(PropTypes.object), + meta: PropTypes.arrayOf(PropTypes.object), + noscript: PropTypes.arrayOf(PropTypes.object), + onChangeClientState: PropTypes.func, + script: PropTypes.arrayOf(PropTypes.object), + style: PropTypes.arrayOf(PropTypes.object), + title: PropTypes.string, + titleAttributes: PropTypes.object, + titleTemplate: PropTypes.string + } + + static defaultProps = { + defer: true, + encodeSpecialCharacters: true + } + + // Component.peek comes from react-side-effect: + // For testing, you may use a static peek() method available on the returned component. + // It lets you get the current state without resetting the mounted instance stack. + // Don’t use it for anything other than testing. + static peek = Component.peek + + static rewind = () => { + let mappedState = Component.rewind() + if (!mappedState) { + // provide fallback if mappedState is undefined + mappedState = mapStateOnServer({ + baseTag: [], + bodyAttributes: {}, + encodeSpecialCharacters: true, + htmlAttributes: {}, + linkTags: [], + metaTags: [], + noscriptTags: [], + scriptTags: [], + styleTags: [], + title: "", + titleAttributes: {} + }) + } + + return mappedState + } + + static set canUseDOM(canUseDOM) { + Component.canUseDOM = canUseDOM + } + + shouldComponentUpdate(nextProps) { + return !isEqual(this.props, nextProps) + } + + mapNestedChildrenToProps(child, nestedChildren) { + if (!nestedChildren) { + return null + } + + switch (child.type) { + case TAG_NAMES.SCRIPT: + case TAG_NAMES.NOSCRIPT: + return { + innerHTML: nestedChildren + } + + case TAG_NAMES.STYLE: + return { + cssText: nestedChildren + } + } + + throw new Error( + `<${child.type} /> elements are self-closing and can not contain children. Refer to our API for more information.` + ) + } + + flattenArrayTypeChildren({ + child, + arrayTypeChildren, + newChildProps, + nestedChildren + }) { + return { + ...arrayTypeChildren, + [child.type]: [ + ...(arrayTypeChildren[child.type] || []), + { + ...newChildProps, + ...this.mapNestedChildrenToProps(child, nestedChildren) + } + ] + } + } + + mapObjectTypeChildren({ + child, + newProps, + newChildProps, + nestedChildren + }) { + switch (child.type) { + case TAG_NAMES.TITLE: + return { + ...newProps, + [child.type]: nestedChildren, + titleAttributes: {...newChildProps} + } + + case TAG_NAMES.BODY: + return { + ...newProps, + bodyAttributes: {...newChildProps} + } + + case TAG_NAMES.HTML: + return { + ...newProps, + htmlAttributes: {...newChildProps} + } + } + + return { + ...newProps, + [child.type]: {...newChildProps} + } + } + + mapArrayTypeChildrenToProps(arrayTypeChildren, newProps) { + let newFlattenedProps = {...newProps} + + Object.keys(arrayTypeChildren).forEach(arrayChildName => { + newFlattenedProps = { + ...newFlattenedProps, + [arrayChildName]: arrayTypeChildren[arrayChildName] + } + }) + + return newFlattenedProps + } + + warnOnInvalidChildren(child, nestedChildren) { + if (process.env.NODE_ENV !== "production") { + if (!VALID_TAG_NAMES.some(name => child.type === name)) { + if (typeof child.type === "function") { + return warn( + `You may be attempting to nest components within each other, which is not allowed. Refer to our API for more information.` + ) + } + + return warn( + `Only elements types ${VALID_TAG_NAMES.join( + ", " + )} are allowed. Helmet does not support rendering <${ + child.type + }> elements. Refer to our API for more information.` + ) + } + + if ( + nestedChildren && + typeof nestedChildren !== "string" && + (!Array.isArray(nestedChildren) || + nestedChildren.some( + nestedChild => typeof nestedChild !== "string" + )) + ) { + throw new Error( + `Helmet expects a string as a child of <${ + child.type + }>. Did you forget to wrap your children in braces? ( <${ + child.type + }>{\`\`} ) Refer to our API for more information.` + ) + } + } + + return true + } + + mapChildrenToProps(children, newProps) { + let arrayTypeChildren = {} + + React.Children.forEach(children, child => { + if (!child || !child.props) { + return + } + + const {children: nestedChildren, ...childProps} = child.props + const newChildProps = convertReactPropstoHtmlAttributes( + childProps + ) + + this.warnOnInvalidChildren(child, nestedChildren) + + switch (child.type) { + case TAG_NAMES.LINK: + case TAG_NAMES.META: + case TAG_NAMES.NOSCRIPT: + case TAG_NAMES.SCRIPT: + case TAG_NAMES.STYLE: + arrayTypeChildren = this.flattenArrayTypeChildren({ + child, + arrayTypeChildren, + newChildProps, + nestedChildren + }) + break + + default: + newProps = this.mapObjectTypeChildren({ + child, + newProps, + newChildProps, + nestedChildren + }) + break + } + }) + + newProps = this.mapArrayTypeChildrenToProps( + arrayTypeChildren, + newProps + ) + return newProps + } + + render() { + const {children, ...props} = this.props + let newProps = {...props} + + if (children) { + newProps = this.mapChildrenToProps(children, newProps) + } + + return + } + } + +const NullComponent = () => null + +const HelmetSideEffects = withSideEffect( + reducePropsToState, + handleClientStateChange, + mapStateOnServer +)(NullComponent) + +const HelmetExport = Helmet(HelmetSideEffects) +HelmetExport.renderStatic = HelmetExport.rewind + +export { HelmetExport as Helmet } +export default HelmetExport diff --git a/webapi/ClientApp/src/components/ReactSideEffect/LICENSE b/webapi/ClientApp/src/components/ReactSideEffect/LICENSE new file mode 100644 index 0000000..af2353d --- /dev/null +++ b/webapi/ClientApp/src/components/ReactSideEffect/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Dan Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/webapi/ClientApp/src/components/ReactSideEffect/README.md b/webapi/ClientApp/src/components/ReactSideEffect/README.md new file mode 100644 index 0000000..0cb3e22 --- /dev/null +++ b/webapi/ClientApp/src/components/ReactSideEffect/README.md @@ -0,0 +1,140 @@ +# React Side Effect [![Downloads](https://img.shields.io/npm/dm/react-side-effect.svg)](https://npmjs.com/react-side-effect) [![npm version](https://img.shields.io/npm/v/react-side-effect.svg?style=flat)](https://www.npmjs.com/package/react-side-effect) + +Create components whose prop changes map to a global side effect. + +## Installation + +``` +npm install --save react-side-effect +``` + +### As a script tag + +#### Development + +```html + + +``` + +#### Production + +```html + + +``` + +## Use Cases + +* Setting `document.body.style.margin` or background color depending on current screen; +* Firing Flux actions using declarative API depending on current screen; +* Some crazy stuff I haven't thought about. + +## How's That Different from `componentDidUpdate`? + +It gathers current props across *the whole tree* before passing them to side effect. For example, this allows you to create `` component like this: + +```jsx +// RootComponent.js +return ( + + {this.state.something ? : } + +); + +// SomeComponent.js +return ( + +
Choose color:
+
+); +``` + +and let the effect handler merge `style` from different level of nesting with innermost winning: + +```js +import { Component, Children } from 'react'; +import PropTypes from 'prop-types'; +import withSideEffect from 'react-side-effect'; + +class BodyStyle extends Component { + render() { + return Children.only(this.props.children); + } +} + +BodyStyle.propTypes = { + style: PropTypes.object.isRequired +}; + +function reducePropsToState(propsList) { + var style = {}; + propsList.forEach(function (props) { + Object.assign(style, props.style); + }); + return style; +} + +function handleStateChangeOnClient(style) { + Object.assign(document.body.style, style); +} + +export default withSideEffect( + reducePropsToState, + handleStateChangeOnClient +)(BodyStyle); +``` + +On the server, you’ll be able to call `BodyStyle.peek()` to get the current state, and `BodyStyle.rewind()` to reset for each next request. The `handleStateChangeOnClient` will only be called on the client. + +## API + +#### `withSideEffect: (reducePropsToState, handleStateChangeOnClient, [mapStateOnServer]) -> ReactComponent -> ReactComponent` + +A [higher-order component](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750) that, when mounting, unmounting or receiving new props, calls `reducePropsToState` with `props` of **each mounted instance**. It is up to you to return some state aggregated from these props. + +On the client, every time the returned component is (un)mounted or its props change, `reducePropsToState` will be called, and the recalculated state will be passed to `handleStateChangeOnClient` where you may use it to trigger a side effect. + +On the server, `handleStateChangeOnClient` will not be called. You will still be able to call the static `rewind()` method on the returned component class to retrieve the current state after a `renderToString()` call. If you forget to call `rewind()` right after `renderToString()`, the internal instance stack will keep growing, resulting in a memory leak and incorrect information. You must call `rewind()` after every `renderToString()` call on the server. + +For testing, you may use a static `peek()` method available on the returned component. It lets you get the current state without resetting the mounted instance stack. Don’t use it for anything other than testing. + +## Usage + +Here's how to implement [React Document Title](https://github.com/gaearon/react-document-title) (both client and server side) using React Side Effect: + +```js +import React, { Children, Component } from 'react'; +import PropTypes from 'prop-types'; +import withSideEffect from 'react-side-effect'; + +class DocumentTitle extends Component { + render() { + if (this.props.children) { + return Children.only(this.props.children); + } else { + return null; + } + } +} + +DocumentTitle.propTypes = { + title: PropTypes.string.isRequired +}; + +function reducePropsToState(propsList) { + var innermostProps = propsList[propsList.length - 1]; + if (innermostProps) { + return innermostProps.title; + } +} + +function handleStateChangeOnClient(title) { + document.title = title || ''; +} + +export default withSideEffect( + reducePropsToState, + handleStateChangeOnClient +)(DocumentTitle); +``` diff --git a/webapi/ClientApp/src/components/ReactSideEffect/index.js b/webapi/ClientApp/src/components/ReactSideEffect/index.js new file mode 100644 index 0000000..aa01e13 --- /dev/null +++ b/webapi/ClientApp/src/components/ReactSideEffect/index.js @@ -0,0 +1,92 @@ +import React, { PureComponent } from 'react' + +const canUseDOM = !!( + typeof window !== 'undefined' && + window.document && + window.document.createElement +) + +export default function withSideEffect( + reducePropsToState, + handleStateChangeOnClient, + mapStateOnServer +) { + if (typeof reducePropsToState !== 'function') { + throw new Error('Expected reducePropsToState to be a function.') + } + if (typeof handleStateChangeOnClient !== 'function') { + throw new Error('Expected handleStateChangeOnClient to be a function.') + } + if (typeof mapStateOnServer !== 'undefined' && typeof mapStateOnServer !== 'function') { + throw new Error('Expected mapStateOnServer to either be undefined or a function.') + } + + function getDisplayName(WrappedComponent) { + return WrappedComponent.displayName || WrappedComponent.name || 'Component' + } + + return function wrap(WrappedComponent) { + if (typeof WrappedComponent !== 'function') { + throw new Error('Expected WrappedComponent to be a React component.') + } + + let mountedInstances = [] + let state + + function emitChange() { + state = reducePropsToState(mountedInstances.map(function (instance) { + return instance.props + })) + + if (SideEffect.canUseDOM) { + handleStateChangeOnClient(state) + } else if (mapStateOnServer) { + state = mapStateOnServer(state) + } + } + + class SideEffect extends PureComponent { + // Try to use displayName of wrapped component + static displayName = `SideEffect(${getDisplayName(WrappedComponent)})` + + // Expose canUseDOM so tests can monkeypatch it + static canUseDOM = canUseDOM + + static peek() { + return state + } + + static rewind() { + if (SideEffect.canUseDOM) { + throw new Error('You may only call rewind() on the server. Call peek() to read the current state.') + } + + let recordedState = state + state = undefined + mountedInstances = [] + return recordedState + } + + UNSAFE_componentWillMount() { + mountedInstances.push(this) + emitChange() + } + + componentDidUpdate() { + emitChange() + } + + componentWillUnmount() { + const index = mountedInstances.indexOf(this) + mountedInstances.splice(index, 1) + emitChange() + } + + render() { + return + } + } + + return SideEffect + } +} diff --git a/webapi/ClientApp/src/functions/dateFormat.ts b/webapi/ClientApp/src/functions/dateFormat.ts deleted file mode 100644 index fff77b3..0000000 --- a/webapi/ClientApp/src/functions/dateFormat.ts +++ /dev/null @@ -1,14 +0,0 @@ -import dayjs from 'dayjs' -import 'dayjs/locale/it' -import 'dayjs/locale/en' -import 'dayjs/locale/ru' - -const dateFormat = (date: string): string => { - return dayjs(date) - .locale('en') - .format("MMMM YYYY, dddd") -} - -export { - dateFormat -} \ No newline at end of file diff --git a/webapi/ClientApp/src/functions/dateTimeFormat.ts b/webapi/ClientApp/src/functions/dateTimeFormat.ts new file mode 100644 index 0000000..40a5a96 --- /dev/null +++ b/webapi/ClientApp/src/functions/dateTimeFormat.ts @@ -0,0 +1,66 @@ +import dayjs from 'dayjs' +import 'dayjs/locale/it' +import 'dayjs/locale/en' +import 'dayjs/locale/ru' + + +// default presets +interface ILocale { + dateFormat?: string, + timeFormat?: string +} + +interface ILocales { + [key: string]: ILocale +} + +const locales: ILocales = { + en: { + dateFormat: "MMMM YYYY, dddd", + timeFormat: "hh:mm" + }, + it: { + dateFormat: "MMMM YYYY, dddd", + timeFormat: "HH:mm" + }, + ru: { + dateFormat: "MMMM YYYY, dddd", + timeFormat: "HH:mm" + } +} + +enum FormatType { + Date, + Time +} + +const dateTimeFormat = (formatType: FormatType, value: string, locale?: string, format?: string) => { + + // fallback in case provided value is not managed + if(!locale || (locale && !Object.keys(locales).includes(locale))) { + locale = Object.keys(locales)[0] + + switch(formatType) { + case FormatType.Date: + format = locales[locale].dateFormat + break + case FormatType.Time: + format = locales[locale].timeFormat + } + } + + return dayjs(value) + .locale(locale) + .format(format) +} + +const dateFormat = (value: string, locale?: string, format?: string): string => + dateTimeFormat(FormatType.Date, value, locale, format) + +const timeFormat = (value: string, locale?: string, format?: string): string => + dateTimeFormat(FormatType.Time, value, locale, format) + +export { + dateFormat, + timeFormat +} \ No newline at end of file diff --git a/webapi/ClientApp/src/functions/index.ts b/webapi/ClientApp/src/functions/index.ts index e636104..aecf62a 100644 --- a/webapi/ClientApp/src/functions/index.ts +++ b/webapi/ClientApp/src/functions/index.ts @@ -1,9 +1,10 @@ -import { dateFormat } from './dateFormat' +import { dateFormat, timeFormat } from './dateTimeFormat' import { findRoutes } from './findRoutes' import { getKeyValue } from './getKeyValue' export { getKeyValue, dateFormat, + timeFormat, findRoutes } \ No newline at end of file diff --git a/webapi/ClientApp/src/layouts/public/NavMenu/index.tsx b/webapi/ClientApp/src/layouts/public/NavMenu/index.tsx index 5beabf5..0cc9944 100644 --- a/webapi/ClientApp/src/layouts/public/NavMenu/index.tsx +++ b/webapi/ClientApp/src/layouts/public/NavMenu/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, useState } from 'react' +import React, { FC, useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { useSelector } from 'react-redux' import { Collapse, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap' @@ -8,7 +8,7 @@ import { MenuItemModel } from '../../../models' const NavMenu : FC = () => { - const content = useSelector((state: ApplicationState) => state.content) + const { content, shopCart } = useSelector((state: ApplicationState) => state) const [state, hookState] = useState({ isOpen: false @@ -20,6 +20,13 @@ const NavMenu : FC = () => { }) } + const titleFormatter = (title: string = ''): string => { + if(title?.includes('{quantity}')) + return title.replace('{quantity}', `${shopCart?.items ? shopCart.items.length : 0}`) + + return title + } + return
{content?.siteName} @@ -28,19 +35,13 @@ const NavMenu : FC = () => {
    {content?.topMenu ? content.topMenu.map((item: MenuItemModel, index: number) => { return - - {item.icon ? <> : ''}{item.title} + <> + {item.icon ? <> : ''}{titleFormatter(item.title)} }) : ''}
- - {/*
- -
*/}
} diff --git a/webapi/ClientApp/src/models/abstractions.ts b/webapi/ClientApp/src/models/abstractions.ts index c3dee6c..fe3ec07 100644 --- a/webapi/ClientApp/src/models/abstractions.ts +++ b/webapi/ClientApp/src/models/abstractions.ts @@ -7,16 +7,6 @@ export interface RequestModel { export interface ResponseModel { } - -export interface PageModel { - -} - -export interface PageSectionModel { - title?: string - text?: string -} - export interface AddressPageSectionModel extends PageSectionModel { firstName: FormItemModel, lastName: FormItemModel, @@ -28,6 +18,14 @@ export interface AddressPageSectionModel extends PageSectionModel { zip: FormItemModel } +export interface PageModel { + +} + +export interface PageSectionModel { + title?: string + text?: string +} export interface PersonModel { id: string, diff --git a/webapi/ClientApp/src/models/index.ts b/webapi/ClientApp/src/models/index.ts index 8d9b016..d7c80a2 100644 --- a/webapi/ClientApp/src/models/index.ts +++ b/webapi/ClientApp/src/models/index.ts @@ -1,5 +1,13 @@ import { PersonModel, PostItemModel } from "./abstractions" + +export interface PaginationModel { + totalPages: number, + currentPage: number, + items: T [] +} + + export interface AuthorModel extends PersonModel { nickName: string } @@ -37,6 +45,20 @@ export interface ImageModel { alt: string } +export interface LinkModel { + target: string, + anchorText: string +} + +export interface LocalizationModel { + timeZone: string, + locale: string, + dateFormat: string, + timeFormat: string, + currency: string, + currencySymbol: string +} + export interface MenuItemModel { icon?: string, title?: string, @@ -44,11 +66,6 @@ export interface MenuItemModel { childItems?: MenuItemModel [] } -export interface LinkModel { - target: string, - anchorText: string -} - export interface ReviewerModel extends PersonModel { fullName: string, position: string @@ -63,6 +80,7 @@ export interface RouteModel { export interface ShopItemModel extends PostItemModel { images?: ImageModel [], sku: string, + brandName: string, rating?: number, price: number, newPrice?: number, @@ -74,8 +92,4 @@ export interface TestimonialModel { reviewer: ReviewerModel } -export interface PaginationModel { - totalPages: number, - currentPage: number, - items: T [] -} + diff --git a/webapi/ClientApp/src/models/pageSections.ts b/webapi/ClientApp/src/models/pageSections.ts index ab4edd5..e1c00ad 100644 --- a/webapi/ClientApp/src/models/pageSections.ts +++ b/webapi/ClientApp/src/models/pageSections.ts @@ -28,7 +28,9 @@ export interface ProductSectionModel extends PageSectionModel { addToCart: string } -export interface RelatedProductsSectionModel extends PageSectionModel {} +export interface RelatedProductsSectionModel extends PageSectionModel { + addToCart: string +} export interface TestimonialsSectionModel extends PageSectionModel { items: TestimonialModel [] @@ -54,6 +56,10 @@ export interface BillingAddressSectionModel extends AddressPageSectionModel { } export interface ShippingAddressSectionModel extends AddressPageSectionModel { } +export interface ShopItemsSectionModel extends PageSectionModel { + addToCart: string +} + export interface CheckoutSummarySectionModel extends PageSectionModel { title: string, total: string, diff --git a/webapi/ClientApp/src/models/pages.ts b/webapi/ClientApp/src/models/pages.ts index d3ea0fa..58d104f 100644 --- a/webapi/ClientApp/src/models/pages.ts +++ b/webapi/ClientApp/src/models/pages.ts @@ -21,7 +21,8 @@ export interface HomePageModel extends PageModel { } export interface ShopCatalogPageModel extends PageModel { - titleSection: PageSection.TitleSectionModel + titleSection: PageSection.TitleSectionModel, + shopItemsSection: PageSection.ShopItemsSectionModel } export interface SignInPageModel extends PageModel { @@ -49,12 +50,12 @@ export interface ShopItemPageModel extends PageModel { relatedProductsSection: PageSection.RelatedProductsSectionModel } -export interface CartPageModel extends PageModel { +export interface ShopCartPageModel extends PageModel { titleSection: PageSection.TitleSectionModel productsSection: PageSection.CartProductsSectionModel } -export interface CheckoutPageModel extends PageModel { +export interface ShopCheckoutPageModel extends PageModel { titleSection: PageSection.TitleSectionModel, billingAddressSection: PageSection.BillingAddressSectionModel, shippingAddressSection: PageSection.ShippingAddressSectionModel, diff --git a/webapi/ClientApp/src/models/responses.ts b/webapi/ClientApp/src/models/responses.ts index 8f904b5..710b3e6 100644 --- a/webapi/ClientApp/src/models/responses.ts +++ b/webapi/ClientApp/src/models/responses.ts @@ -1,16 +1,6 @@ -import { BlogItemModel, CategoryModel, CommentModel, MenuItemModel, PaginationModel, RouteModel, ShopItemModel } from "./" +import { BlogItemModel, CategoryModel, CommentModel, LocalizationModel, MenuItemModel, PaginationModel, RouteModel, ShopItemModel } from "./" import { ResponseModel } from "./abstractions" -import { - HomePageModel, - BlogCatalogPageModel, - BlogItemPageModel, - ShopCatalogPageModel, - ShopItemPageModel, - SignInPageModel, - SignUpPageModel, - CartPageModel, - CheckoutPageModel -} from "./pages" +import * as Pages from "./pages" @@ -35,13 +25,17 @@ export interface GetShopRelatedResponseModel extends ResponseModel { } export interface GetShopCartResponseModel extends ResponseModel { - quantity: number + items: ShopItemModel [] } // Static content response model export interface GetContentResponseModel extends ResponseModel { siteName: string, + + helmet: any, + + localization: LocalizationModel, routes: RouteModel [], adminRoutes: RouteModel [], @@ -50,18 +44,18 @@ export interface GetContentResponseModel extends ResponseModel { topMenu: MenuItemModel [], sideMenu: MenuItemModel [], - homePage: HomePageModel, + homePage: Pages.HomePageModel, - shopCatalog: ShopCatalogPageModel, - shopItem: ShopItemPageModel, - cart: CartPageModel, - checkout: CheckoutPageModel, + shopCatalog: Pages.ShopCatalogPageModel, + shopItem: Pages.ShopItemPageModel, + shopCart: Pages.ShopCartPageModel, + shopCheckout: Pages.ShopCheckoutPageModel, - blogCatalog: BlogCatalogPageModel, - blogItem: BlogItemPageModel, + blogCatalog: Pages.BlogCatalogPageModel, + blogItem: Pages.BlogItemPageModel, - signIn: SignInPageModel, - signUp: SignUpPageModel + signIn: Pages.SignInPageModel, + signUp: Pages.SignUpPageModel } // Blog response models diff --git a/webapi/ClientApp/src/pages/Blog/Catalog/index.tsx b/webapi/ClientApp/src/pages/Blog/Catalog/index.tsx index f45fceb..c082958 100644 --- a/webapi/ClientApp/src/pages/Blog/Catalog/index.tsx +++ b/webapi/ClientApp/src/pages/Blog/Catalog/index.tsx @@ -167,7 +167,7 @@ const BlogCatalog = () => { const blogItem = blogFeatured?.items[0] const featuredBlog: FeaturedBlog = { - path: path, + path, currentPage: blogCatalog?.currentPage, item: blogItem, readTime: page?.featuredBlogSection?.readTime diff --git a/webapi/ClientApp/src/pages/Shop/Cart/index.tsx b/webapi/ClientApp/src/pages/Shop/Cart/index.tsx index dd659f5..eee3082 100644 --- a/webapi/ClientApp/src/pages/Shop/Cart/index.tsx +++ b/webapi/ClientApp/src/pages/Shop/Cart/index.tsx @@ -1,49 +1,97 @@ -import React from 'react' +// React +import React, { useEffect, useState } from 'react' + +// Redux +import { useDispatch, useSelector } from 'react-redux' +// import { actionCreator as shopCartActionCreator } from '../../../store/reducers/ShopCart' +import { ApplicationState } from '../../../store' + import { Container } from 'reactstrap' import { FeatherIcon } from '../../../components/FeatherIcons' import style from './scss/style.module.scss' + const Cart = () => { + const dispatch = useDispatch() + const { content, shopCart } = useSelector((state: ApplicationState) => state) + + const { + currencySymbol = "" + } = content?.localization ? content.localization : {} + + const { + titleSection = { + title: "", + text: "" + }, + productsSection = { + product: "", + price: "", + quantity: "", + subtotal: "", + continueShopping: { + target: "#", + anchorText: "" + }, + submit: { + title: "" + } + } + + } = content?.shopCart ? content.shopCart : {} + + const [subtotal, setSubtotal] = useState(0) + + useEffect(() => { + if(shopCart?.items) { + let newSubtotal = 0 + shopCart.items.forEach(item => { + if(item.quantity) + newSubtotal += (item.newPrice ? item.newPrice : item.price) * item.quantity + }) + setSubtotal(newSubtotal) + } + + }, [shopCart?.items]) + return
-

Shopping Cart

-

- 3 items in your cart

+

{titleSection.title}

+

{shopCart?.items ? shopCart.items.length : 0} {titleSection.text}

- - - + + + - {[{}, {}, {}, {}].map((item, index) => + {(shopCart?.items ? shopCart.items : []).map((item, index) => - +
ProductPriceQuantity{productsSection.product}{productsSection.price}{productsSection.quantity}
-

Product Name

-

Brand & Name

+

{item.title}

+

{item.brandName}

$49.00{item.newPrice + ? <>{currencySymbol}{item.price.toFixed(2)} {currencySymbol}{item.newPrice.toFixed(2)} + : {currencySymbol}{item.price.toFixed(2)}} - +
- @@ -55,18 +103,18 @@ const Cart = () => {
-

Subtotal:

-

$99.00

+

{productsSection.subtotal}

+

{currencySymbol}{subtotal}

diff --git a/webapi/ClientApp/src/pages/Shop/Catalog/index.tsx b/webapi/ClientApp/src/pages/Shop/Catalog/index.tsx index d7c15ad..4fa6fa6 100644 --- a/webapi/ClientApp/src/pages/Shop/Catalog/index.tsx +++ b/webapi/ClientApp/src/pages/Shop/Catalog/index.tsx @@ -36,13 +36,17 @@ const TitleSection: FC = ({ interface ShopItems { - path?: string + currencySymbol: string, + addToCart: string, + path: string totalPages?: number, currentPage?: number, items?: ShopItemModel [] } const ShopItemsSection: FC = ({ + currencySymbol = "", + addToCart = "", path = "", totalPages = 1, currentPage = 1, @@ -57,7 +61,9 @@ const ShopItemsSection: FC = ({ {items.map((item, index) => -
{item.badges}
+
+ {(item?.badges ? item.badges : []).map((badge, index) =>
{badge}
) } +
@@ -72,18 +78,15 @@ const ShopItemsSection: FC = ({ }} /> {item.newPrice - ? <>{item.price} {item.newPrice} - : {item.price}} - + ? <>{currencySymbol}{item.price.toFixed(2)} {currencySymbol}{item.newPrice.toFixed(2)} + : {currencySymbol}{item.price.toFixed(2)}} - +
)} - -
{ const page = content?.shopCatalog const path = findRoutes(content?.routes, 'ShopCatalog')[0]?.targets[0] + const { + currencySymbol = "" + } = content?.localization ? content.localization : {} + useEffect(() => { dispatch(shopCatalogActionCreators.requestShopCatalog({ currentPage: params?.page ? params.page : "1" @@ -124,8 +131,16 @@ const ShopCatalog = () => { }, 1000) }, [shopCatalog?.isLoading]) + const { + shopItemsSection = { + addToCart: "" + } + } = content?.shopCatalog ? content?.shopCatalog : {} + const shopItems: ShopItems = { + currencySymbol, path, + ...shopItemsSection, ...shopCatalog } diff --git a/webapi/ClientApp/src/pages/Shop/Item/index.tsx b/webapi/ClientApp/src/pages/Shop/Item/index.tsx index b483502..fc00484 100644 --- a/webapi/ClientApp/src/pages/Shop/Item/index.tsx +++ b/webapi/ClientApp/src/pages/Shop/Item/index.tsx @@ -1,22 +1,36 @@ +//React import React, { FC, useEffect } from 'react' import { useParams } from 'react-router-dom' +// Redux import { useDispatch, useSelector } from 'react-redux' +import { ApplicationState } from '../../../store' import { actionCreators as loaderActionCreators } from '../../../store/reducers/Loader' import { actionCreators as shopItemActionCreators } from '../../../store/reducers/ShopItem' +// Reactstrap import { Container } from 'reactstrap' + +// Components import { FeatherIcon } from '../../../components/FeatherIcons' import { RelatedProducts } from '../RelatedProducts' -import { ApplicationState } from '../../../store' - -const ShopItem = () => { +const ShopItem : FC = () => { const params = useParams() const dispatch = useDispatch() const { content, shopItem } = useSelector((state: ApplicationState) => state) - const page = content?.shopItem + + const { + currencySymbol = "" + } = content?.localization ? content.localization : {} + + const { + productSection = { + availableQuantity: "", + addToCart: "" + } + } = content?.shopItem ? content.shopItem : {} useEffect(() => { if(params?.slug) @@ -26,12 +40,15 @@ const ShopItem = () => { }, []) useEffect(() => { - shopItem?.isLoading + content?.isLoading || shopItem?.isLoading ? dispatch(loaderActionCreators.show()) : setTimeout(() => { dispatch(loaderActionCreators.hide()) }, 1000) - }, [shopItem?.isLoading]) + }, [content?.isLoading, shopItem?.isLoading]) + + + return <>
@@ -43,16 +60,19 @@ const ShopItem = () => {

{shopItem?.title}

{shopItem?.newPrice - ? <>{shopItem?.price} {shopItem?.newPrice} - : {shopItem?.price}} + ? <>{currencySymbol}{shopItem.price.toFixed(2)} {currencySymbol}{shopItem.newPrice.toFixed(2)} + : {currencySymbol}{shopItem?.price.toFixed(2)}}
+
{productSection.availableQuantity} {shopItem?.quantity ? shopItem.quantity : 0}
+
+ + {productSection.addToCart}
diff --git a/webapi/ClientApp/src/pages/Shop/RelatedProducts/index.tsx b/webapi/ClientApp/src/pages/Shop/RelatedProducts/index.tsx index 109d284..80c5c3d 100644 --- a/webapi/ClientApp/src/pages/Shop/RelatedProducts/index.tsx +++ b/webapi/ClientApp/src/pages/Shop/RelatedProducts/index.tsx @@ -1,59 +1,77 @@ -import React from "react" +// React +import React, { FC, useEffect } from "react" + +// Reduc +import { useDispatch, useSelector } from "react-redux" +import { ApplicationState } from "../../../store" +import { actionCreators as loaderActionCreators } from '../../../store/reducers/Loader' + +// Reactstrap import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from "reactstrap" +// Components +import { FeatherRating } from "../../../components/FeatherRating" -const RelatedProducts = () => { +const RelatedProducts: FC = () => { + const dispatch = useDispatch() - const items = [ - { - - }, - { - - }, - { - - }, - { + const { content, shopRelated } = useSelector((state: ApplicationState) => state) + + const { + currencySymbol = "" + } = content?.localization ? content.localization : {} + const { + relatedProductsSection = { + title: "", + addToCart: "" } - ] + } = content?.shopItem ? content?.shopItem : {} + + const { + items = [] + } = shopRelated ? shopRelated : {} + + useEffect(() => { + content?.isLoading || shopRelated?.isLoading + ? dispatch(loaderActionCreators.show()) + : setTimeout(() => { + dispatch(loaderActionCreators.hide()) + }, 1000) + }, [content?.isLoading, shopRelated?.isLoading]) return
- -

Related products

+ +

{relatedProductsSection.title}

{items.map((item, index) => -
Sale
- +
+ {(item?.badges ? item.badges : []).map((badge, index) =>
{badge}
) } +
+
-
Special Item
- -
-
-
-
-
-
-
- - $20.00 - $18.00 +
{item.title}
+ + + {item.newPrice + ? <>{currencySymbol}{item.price.toFixed(2)} {currencySymbol}{item.newPrice.toFixed(2)} + : {currencySymbol}{item.price.toFixed(2)}}
- +
)}
- } export { diff --git a/webapi/ClientApp/src/store/index.ts b/webapi/ClientApp/src/store/index.ts index 11ec4de..b3d1a41 100644 --- a/webapi/ClientApp/src/store/index.ts +++ b/webapi/ClientApp/src/store/index.ts @@ -13,6 +13,7 @@ import * as ShopCategories from './reducers/ShopCategories' import * as ShopFeatured from './reducers/ShopFeatured' import * as ShopItem from './reducers/ShopItem' import * as ShopRelated from './reducers/ShopRelated' +import * as ShopCart from './reducers/ShopCart' import * as WeatherForecasts from './reducers/WeatherForecasts' @@ -33,6 +34,7 @@ export interface ApplicationState { shopFeatured: ShopFeatured.ShopFeaturedState | undefined shopItem: ShopItem.ShopItemState | undefined shopRelated: ShopRelated.ShopRelatedState | undefined + shopCart: ShopCart.ShopCartState | undefined weatherForecasts: WeatherForecasts.WeatherForecastsState | undefined } @@ -56,6 +58,7 @@ export const reducers = { shopFeatured: ShopFeatured.reducer, shopItem: ShopItem.reducer, shopRelated: ShopRelated.reducer, + shopCart: ShopCart.reducer, weatherForecasts: WeatherForecasts.reducer } diff --git a/webapi/ClientApp/src/store/reducers/BlogCatalog.ts b/webapi/ClientApp/src/store/reducers/BlogCatalog.ts index 0c7c6ba..cc15083 100644 --- a/webapi/ClientApp/src/store/reducers/BlogCatalog.ts +++ b/webapi/ClientApp/src/store/reducers/BlogCatalog.ts @@ -61,6 +61,78 @@ const unloadedState: BlogCatalogState = { likes: 0 }, + { + id: "", + slug: "demo-post", + badges: [ "demo" ], + image: { + src: "https://dummyimage.com/850x350/dee2e6/6c757d.jpg", + alt: "..." + }, + title: "Lorem ipsum", + shortText: "", + text: "", + author: { + id: "", + nickName: "Admin", + image: { + src: "https://dummyimage.com/40x40/ced4da/6c757d", + alt: "..." + } + }, + created: new Date().toString(), + tags: [], + + likes: 0 + }, + { + id: "", + slug: "demo-post", + badges: [ "demo" ], + image: { + src: "https://dummyimage.com/850x350/dee2e6/6c757d.jpg", + alt: "..." + }, + title: "Lorem ipsum", + shortText: "", + text: "", + author: { + id: "", + nickName: "Admin", + image: { + src: "https://dummyimage.com/40x40/ced4da/6c757d", + alt: "..." + } + }, + created: new Date().toString(), + tags: [], + + likes: 0 + }, + { + id: "", + slug: "demo-post", + badges: [ "demo" ], + image: { + src: "https://dummyimage.com/850x350/dee2e6/6c757d.jpg", + alt: "..." + }, + title: "Lorem ipsum", + shortText: "", + text: "", + author: { + id: "", + nickName: "Admin", + image: { + src: "https://dummyimage.com/40x40/ced4da/6c757d", + alt: "..." + } + }, + created: new Date().toString(), + tags: [], + + likes: 0 + } ], isLoading: false } diff --git a/webapi/ClientApp/src/store/reducers/Cart.ts b/webapi/ClientApp/src/store/reducers/Cart.ts deleted file mode 100644 index 17116f2..0000000 --- a/webapi/ClientApp/src/store/reducers/Cart.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Action, Reducer } from 'redux' -import { AppThunkAction } from '..' -import { GetShopCartRequestModel } from '../../models/requests' -import { GetShopCartResponseModel } from '../../models/responses' - -export interface CartState extends GetShopCartResponseModel { - isLoading: boolean -} - -export interface RequestAction extends GetShopCartRequestModel { - type: 'REQUEST_CART' -} - -export interface ReceiveAction extends GetShopCartResponseModel { - type: 'RECEIVE_CART' -} - -export type KnownAction = RequestAction | ReceiveAction - -export const actionCreators = { - requestCart: (): AppThunkAction => (dispatch, getState) => { - - }, - - addToCart: (): AppThunkAction => (dispatch, getState) => { - - // Get>('https://localhost:7151/api/BlogItem', props) - // .then(response => response) - // .then(data => { - // if(data) - // dispatch({ type: 'RECEIVE_BLOG_ITEM', ...data }) - // }) - - // dispatch({ type: 'REQUEST_BLOG_ITEM', slug: props.slug }) - }, - - remFromCart: (): AppThunkAction => (dispatch, getState) => { - - } -} - -const unloadedState: CartState = { - quantity: 0, - - isLoading: false -} - -export const reducer: Reducer = (state: CartState | undefined, incomingAction: Action): CartState => { - if (state === undefined) { - return unloadedState - } - - const action = incomingAction as KnownAction - switch (action.type) { - case 'REQUEST_CART': - return { - ...state, - isLoading: true - } - - case 'RECEIVE_CART': - return { - ...action, - isLoading: false - } - } - - return state -} \ No newline at end of file diff --git a/webapi/ClientApp/src/store/reducers/Content.ts b/webapi/ClientApp/src/store/reducers/Content.ts index b25f242..7c060fa 100644 --- a/webapi/ClientApp/src/store/reducers/Content.ts +++ b/webapi/ClientApp/src/store/reducers/Content.ts @@ -36,6 +36,28 @@ export const actionCreators = { const unloadedState: ContentState = { siteName: "MAKS-IT", + + helmet: { + title: "{siteName}", + meta: { + chartset: "utf-8", + description: "react-redux", + "google-site-verification": "", + robots: "noindex, nofollow" + }, + link: { + canonical: "" + } + }, + + localization: { + timeZone: "+1", + locale: "en-US", + dateFormat: "MMMM YYYY, dddd", + timeFormat: "HH:mm", + currency: "EUR", + currencySymbol: "€" + }, routes: [ { target: "/", component: "Home" }, { target: "/home", component: "Home" }, @@ -137,6 +159,9 @@ const unloadedState: ContentState = { titleSection: { title: "Shop in style", text: "With this shop hompeage template" + }, + shopItemsSection: { + addToCart: "Add to cart" } }, @@ -146,13 +171,15 @@ const unloadedState: ContentState = { addToCart: "Add to cart" }, relatedProductsSection: { - title: "Related products" + title: "Related products", + addToCart: "Add to cart" } }, - cart: { + shopCart: { titleSection: { - title: "Shopping Cart" + title: "Shopping Cart", + text: "items in your cart" }, productsSection: { title: "Shopping Cart", @@ -160,9 +187,9 @@ const unloadedState: ContentState = { product: "Product", price: "Price", quantity: "Quantity", - subtotal: "Subtotal", + subtotal: "Subtotal:", continueShopping: { - target: "#", + target: "/shop", anchorText: "Continue shopping" }, submit: { @@ -171,7 +198,7 @@ const unloadedState: ContentState = { } }, - checkout: { + shopCheckout: { titleSection: { title: "Checkout", text: "Below is an example form built entirely with Bootstrap’s form controls. Each required form group has a validation state that can be triggered by attempting to submit the form without completing it." diff --git a/webapi/ClientApp/src/store/reducers/ShopCart.ts b/webapi/ClientApp/src/store/reducers/ShopCart.ts new file mode 100644 index 0000000..0b62d13 --- /dev/null +++ b/webapi/ClientApp/src/store/reducers/ShopCart.ts @@ -0,0 +1,148 @@ +import { Action, Reducer } from 'redux' +import { AppThunkAction } from '..' +import { GetShopCartRequestModel } from '../../models/requests' +import { GetShopCartResponseModel } from '../../models/responses' + +export interface ShopCartState extends GetShopCartResponseModel { + isLoading: boolean +} + +export interface RequestAction extends GetShopCartRequestModel { + type: 'REQUEST_CART' +} + +export interface ReceiveAction extends GetShopCartResponseModel { + type: 'RECEIVE_CART' +} + +export type KnownAction = RequestAction | ReceiveAction + +export const actionCreators = { + requestCart: (): AppThunkAction => (dispatch, getState) => { + + }, + + addToCart: (): AppThunkAction => (dispatch, getState) => { + + // Get>('https://localhost:7151/api/BlogItem', props) + // .then(response => response) + // .then(data => { + // if(data) + // dispatch({ type: 'RECEIVE_BLOG_ITEM', ...data }) + // }) + + // dispatch({ type: 'REQUEST_BLOG_ITEM', slug: props.slug }) + }, + + remFromCart: (): AppThunkAction => (dispatch, getState) => { + + } +} + +const unloadedState: ShopCartState = { + items: [ + { + id: '', + slug: "shop-catalog-item", + sku: "SKU-0", + image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, + badges: [ "sale" ], + title: "Shop item title", + brandName: "Brand & Name", + + shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", + text: "", + author: { + id: '', + image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." }, + nickName: "Admin" + }, + created: (new Date).toString(), + + tags: [ "react", "redux", "webapi" ], + + rating: 4.5, + price: 20, + newPrice: 10, + + quantity: 1 + }, + { + id: '', + slug: "shop-catalog-item", + sku: "SKU-0", + image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, + badges: [ "sale" ], + title: "Shop item title", + brandName: "Brand & Name", + + shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", + text: "", + author: { + id: '', + image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." }, + nickName: "Admin" + }, + created: (new Date).toString(), + + tags: [ "react", "redux", "webapi" ], + + rating: 4.5, + price: 20, + newPrice: 10, + + quantity: 2 + }, + { + id: '', + slug: "shop-catalog-item", + sku: "SKU-0", + image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, + badges: [ "sale" ], + title: "Shop item title", + brandName: "Brand & Name", + + shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", + text: "", + author: { + id: '', + image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." }, + nickName: "Admin" + }, + created: (new Date).toString(), + + tags: [ "react", "redux", "webapi" ], + + rating: 4.5, + price: 20, + newPrice: 10, + + quantity: 3 + } + ], + + isLoading: false +} + +export const reducer: Reducer = (state: ShopCartState | undefined, incomingAction: Action): ShopCartState => { + if (state === undefined) { + return unloadedState + } + + const action = incomingAction as KnownAction + switch (action.type) { + case 'REQUEST_CART': + return { + ...state, + isLoading: true + } + + case 'RECEIVE_CART': + return { + ...action, + isLoading: false + } + } + + return state +} \ No newline at end of file diff --git a/webapi/ClientApp/src/store/reducers/ShopCatalog.ts b/webapi/ClientApp/src/store/reducers/ShopCatalog.ts index 1fd0660..a97fcc4 100644 --- a/webapi/ClientApp/src/store/reducers/ShopCatalog.ts +++ b/webapi/ClientApp/src/store/reducers/ShopCatalog.ts @@ -44,7 +44,80 @@ const unloadedState: ShopCatalogState = { image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, badges: [ "sale" ], title: "Shop item title", + brandName: "Brand & Name", + + shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", + text: "", + author: { + id: '', + image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." }, + nickName: "Admin" + }, + created: (new Date).toString(), + tags: [ "react", "redux", "webapi" ], + + rating: 4.5, + price: 20, + newPrice: 10 + }, + { + id: '', + slug: "shop-catalog-item", + sku: "SKU-0", + image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, + badges: [ "sale" ], + title: "Shop item title", + brandName: "Brand & Name", + + shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", + text: "", + author: { + id: '', + image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." }, + nickName: "Admin" + }, + created: (new Date).toString(), + + tags: [ "react", "redux", "webapi" ], + + rating: 4.5, + price: 20, + newPrice: 10 + }, + { + id: '', + slug: "shop-catalog-item", + sku: "SKU-0", + image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, + badges: [ "sale", "out of stock" ], + title: "Shop item title", + brandName: "Brand & Name", + + shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", + text: "", + author: { + id: '', + image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." }, + nickName: "Admin" + }, + created: (new Date).toString(), + + tags: [ "react", "redux", "webapi" ], + + rating: 4.5, + price: 20, + newPrice: 10 + }, + { + id: '', + slug: "shop-catalog-item", + sku: "SKU-0", + image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, + badges: [ "sale" ], + title: "Shop item title", + brandName: "Brand & Name", + shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", text: "", author: { diff --git a/webapi/ClientApp/src/store/reducers/ShopFeatured.ts b/webapi/ClientApp/src/store/reducers/ShopFeatured.ts index c4b937d..c837693 100644 --- a/webapi/ClientApp/src/store/reducers/ShopFeatured.ts +++ b/webapi/ClientApp/src/store/reducers/ShopFeatured.ts @@ -42,6 +42,7 @@ const unloadedState: ShopFeaturedState = { image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, badges: [ "sale" ], title: "Shop item title", + brandName: "Brand & Name", shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", text: "", diff --git a/webapi/ClientApp/src/store/reducers/ShopItem.ts b/webapi/ClientApp/src/store/reducers/ShopItem.ts index 51e2a0b..2fc6c6a 100644 --- a/webapi/ClientApp/src/store/reducers/ShopItem.ts +++ b/webapi/ClientApp/src/store/reducers/ShopItem.ts @@ -49,6 +49,8 @@ const unloadedState: ShopItemState = { ], title: "Shop item template", + brandName: "Brand & Name", + text: `

Lorem ipsum dolor sit amet consectetur adipisicing elit. Praesentium at dolorem quidem modi. Nam sequi consequatur obcaecati excepturi alias magni, accusamus eius blanditiis delectus ipsam minima ea iste laborum vero?

`, author: { id: "", diff --git a/webapi/ClientApp/src/store/reducers/ShopRelated.ts b/webapi/ClientApp/src/store/reducers/ShopRelated.ts index 0f47723..75fd9e4 100644 --- a/webapi/ClientApp/src/store/reducers/ShopRelated.ts +++ b/webapi/ClientApp/src/store/reducers/ShopRelated.ts @@ -1,28 +1,28 @@ import { Action, Reducer } from 'redux' import { AppThunkAction } from '../' -import { GetShopCatalogRequestModel } from '../../models/requests' -import { GetShopCatalogResponseModel } from '../../models/responses' +import { GetShopRelatedRequestModel } from '../../models/requests' +import { GetShopRelatedResponseModel } from '../../models/responses' import { Get } from '../../restClient' -export interface ShopRelatedState extends GetShopCatalogResponseModel { +export interface ShopRelatedState extends GetShopRelatedResponseModel { isLoading: boolean } -interface RequestAction extends GetShopCatalogRequestModel { +interface RequestAction extends GetShopRelatedRequestModel { type: 'REQUEST_SHOP_RELATED' } -interface ReceiveAction extends GetShopCatalogResponseModel { +interface ReceiveAction extends GetShopRelatedResponseModel { type: 'RECEIVE_SHOP_RELATED' } type KnownAction = RequestAction | ReceiveAction export const actionCreators = { - requestShopRelated: (props?: GetShopCatalogRequestModel): AppThunkAction => (dispatch, getState) => { + requestShopRelated: (props?: GetShopRelatedRequestModel): AppThunkAction => (dispatch, getState) => { - Get>('https://localhost:7151/api/ShopRelated', props) + Get>('https://localhost:7151/api/ShopRelated', props) .then(response => response) .then(data => { if(data) @@ -34,17 +34,88 @@ export const actionCreators = { } const unloadedState: ShopRelatedState = { - totalPages: 1, - currentPage: 1, items: [ { id: '', slug: "shop-catalog-item", sku: "SKU-0", image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, - badges: [ "sale" ], + badges: [ "sale", "best offer" ], title: "Shop item title", + brandName: "Brand & Name", + + shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", + text: "", + author: { + id: '', + image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." }, + nickName: "Admin" + }, + created: (new Date).toString(), + tags: [ "react", "redux", "webapi" ], + + rating: 4.5, + price: 20, + newPrice: 10 + }, + { + id: '', + slug: "shop-catalog-item", + sku: "SKU-0", + image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, + badges: [ "sale", "best offer" ], + title: "Shop item title", + brandName: "Brand & Name", + + shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", + text: "", + author: { + id: '', + image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." }, + nickName: "Admin" + }, + created: (new Date).toString(), + + tags: [ "react", "redux", "webapi" ], + + rating: 4.5, + price: 20, + newPrice: 10 + }, + { + id: '', + slug: "shop-catalog-item", + sku: "SKU-0", + image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, + badges: [ "sale", "best offer" ], + title: "Shop item title", + brandName: "Brand & Name", + + shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", + text: "", + author: { + id: '', + image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." }, + nickName: "Admin" + }, + created: (new Date).toString(), + + tags: [ "react", "redux", "webapi" ], + + rating: 4.5, + price: 20, + newPrice: 10 + }, + { + id: '', + slug: "shop-catalog-item", + sku: "SKU-0", + image: { src: "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", alt: "..." }, + badges: [ "sale", "best offer" ], + title: "Shop item title", + brandName: "Brand & Name", + shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...", text: "", author: { diff --git a/webapi/WeatherForecast/Models/Abstractions/AddressPageSectionModel.cs b/webapi/WeatherForecast/Models/Abstractions/AddressPageSectionModel.cs new file mode 100644 index 0000000..8126a3e --- /dev/null +++ b/webapi/WeatherForecast/Models/Abstractions/AddressPageSectionModel.cs @@ -0,0 +1,20 @@ +namespace WeatherForecast.Models.Abstractions { + public abstract class AddressPageSectionModel : PageSectionModel { + public FormItemModel FirstName { get; set; } + + public FormItemModel LastName { get; set; } + + public FormItemModel Address { get; set; } + + public FormItemModel Address2 { get; set; } + + public FormItemModel Country { get; set; } + + public FormItemModel State { get; set; } + + public FormItemModel City { get; set; } + + public FormItemModel Zip { get; set; } + } + +} diff --git a/webapi/WeatherForecast/Models/LinkModel.cs b/webapi/WeatherForecast/Models/LinkModel.cs new file mode 100644 index 0000000..4326d51 --- /dev/null +++ b/webapi/WeatherForecast/Models/LinkModel.cs @@ -0,0 +1,6 @@ +namespace WeatherForecast.Models { + public class LinkModel { + public string Target { get; set; } + public string AnchorText { get; set; } + } +} diff --git a/webapi/WeatherForecast/Models/LocalizationModel.cs b/webapi/WeatherForecast/Models/LocalizationModel.cs new file mode 100644 index 0000000..a66a9cc --- /dev/null +++ b/webapi/WeatherForecast/Models/LocalizationModel.cs @@ -0,0 +1,15 @@ +namespace WeatherForecast.Models { + public class LocalizationModel { + public string TimeZone { get; set; } + + public string Locale { get; set; } + + public string DateFormat { get; set; } + + public string TimeFormat { get; set; } + + public string Currency { get; set; } + + public string CurrencySymobol { get; set; } + } +}