(feat): react helmet
This commit is contained in:
parent
25da28f001
commit
901b7f02e3
@ -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 <>
|
||||
<Helmet>
|
||||
<title>{siteName}</title>
|
||||
<meta charSet="utf-8" />
|
||||
|
||||
<link rel="canonical" href="http://mysite.com/example" />
|
||||
|
||||
<meta name="description" content="react-redux" />
|
||||
</Helmet>
|
||||
|
||||
<Routes>
|
||||
{content?.routes ? NestedRoutes(content.routes, 'PublicLayout') : ''}
|
||||
{content?.adminRoutes ? NestedRoutes(content.adminRoutes, 'AdminLayout') : ''}
|
||||
|
||||
22
webapi/ClientApp/src/components/ReactFastCompare/LICENSE
Normal file
22
webapi/ClientApp/src/components/ReactFastCompare/LICENSE
Normal file
@ -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.
|
||||
165
webapi/ClientApp/src/components/ReactFastCompare/README.md
Normal file
165
webapi/ClientApp/src/components/ReactFastCompare/README.md
Normal file
@ -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.
|
||||
|
||||

|
||||
|
||||
(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
|
||||
126
webapi/ClientApp/src/components/ReactFastCompare/index.js
Normal file
126
webapi/ClientApp/src/components/ReactFastCompare/index.js
Normal file
@ -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<Type> &&` 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
|
||||
@ -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'
|
||||
657
webapi/ClientApp/src/components/ReactHelmet/HelmetUtils.js
Normal file
657
webapi/ClientApp/src/components/ReactHelmet/HelmetUtils.js
Normal file
@ -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, '"')
|
||||
.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}>`
|
||||
: `<${type} ${HELMET_ATTRIBUTE}="true">${encodeSpecialCharacters(
|
||||
flattenedTitle,
|
||||
encode
|
||||
)}</${type}>`
|
||||
}
|
||||
|
||||
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}</${type}>`
|
||||
}`
|
||||
}, '')
|
||||
|
||||
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 }
|
||||
19
webapi/ClientApp/src/components/ReactHelmet/LICENSE
Normal file
19
webapi/ClientApp/src/components/ReactHelmet/LICENSE
Normal file
@ -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.
|
||||
269
webapi/ClientApp/src/components/ReactHelmet/README.md
Normal file
269
webapi/ClientApp/src/components/ReactHelmet/README.md
Normal file
@ -0,0 +1,269 @@
|
||||
<img align="right" width="200" src="http://static.nfl.com/static/content/public/static/img/logos/react-helmet.jpg" />
|
||||
|
||||
# React Helmet
|
||||
|
||||
[](https://www.npmjs.org/package/react-helmet)
|
||||
[](https://codecov.io/github/nfl/react-helmet?branch=master)
|
||||
[](https://travis-ci.org/nfl/react-helmet)
|
||||
[](https://david-dm.org/nfl/react-helmet)
|
||||
[](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 (
|
||||
<div className="application">
|
||||
<Helmet>
|
||||
<meta charSet="utf-8" />
|
||||
<title>My Title</title>
|
||||
<link rel="canonical" href="http://mysite.com/example" />
|
||||
</Helmet>
|
||||
...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Nested or latter components will override duplicate changes:
|
||||
|
||||
```javascript
|
||||
<Parent>
|
||||
<Helmet>
|
||||
<title>My Title</title>
|
||||
<meta name="description" content="Helmet application" />
|
||||
</Helmet>
|
||||
|
||||
<Child>
|
||||
<Helmet>
|
||||
<title>Nested Title</title>
|
||||
<meta name="description" content="Nested component" />
|
||||
</Helmet>
|
||||
</Child>
|
||||
</Parent>
|
||||
```
|
||||
|
||||
outputs:
|
||||
|
||||
```html
|
||||
<head>
|
||||
<title>Nested Title</title>
|
||||
<meta name="description" content="Nested component">
|
||||
</head>
|
||||
```
|
||||
|
||||
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(<Handler />);
|
||||
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 = `
|
||||
<!doctype html>
|
||||
<html ${helmet.htmlAttributes.toString()}>
|
||||
<head>
|
||||
${helmet.title.toString()}
|
||||
${helmet.meta.toString()}
|
||||
${helmet.link.toString()}
|
||||
</head>
|
||||
<body ${helmet.bodyAttributes.toString()}>
|
||||
<div id="content">
|
||||
// React stuff here
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
```
|
||||
|
||||
### As React components
|
||||
```javascript
|
||||
function HTML () {
|
||||
const htmlAttrs = helmet.htmlAttributes.toComponent();
|
||||
const bodyAttrs = helmet.bodyAttributes.toComponent();
|
||||
|
||||
return (
|
||||
<html {...htmlAttrs}>
|
||||
<head>
|
||||
{helmet.title.toComponent()}
|
||||
{helmet.meta.toComponent()}
|
||||
{helmet.link.toComponent()}
|
||||
</head>
|
||||
<body {...bodyAttrs}>
|
||||
<div id="content">
|
||||
// React stuff here
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
<Helmet
|
||||
{/* (optional) set to false to disable string encoding (server-only) */}
|
||||
encodeSpecialCharacters={true}
|
||||
|
||||
{/*
|
||||
(optional) Useful when you want titles to inherit from a template:
|
||||
|
||||
<Helmet
|
||||
titleTemplate="%s | MyAwesomeWebsite.com"
|
||||
>
|
||||
<title>Nested Title</title>
|
||||
</Helmet>
|
||||
|
||||
outputs:
|
||||
|
||||
<head>
|
||||
<title>Nested Title | MyAwesomeWebsite.com</title>
|
||||
</head>
|
||||
*/}
|
||||
titleTemplate="MySite.com - %s"
|
||||
|
||||
{/*
|
||||
(optional) used as a fallback when a template exists but a title is not defined
|
||||
|
||||
<Helmet
|
||||
defaultTitle="My Site"
|
||||
titleTemplate="My Site - %s"
|
||||
/>
|
||||
|
||||
outputs:
|
||||
|
||||
<head>
|
||||
<title>My Site</title>
|
||||
</head>
|
||||
*/}
|
||||
defaultTitle="My Default Title"
|
||||
|
||||
{/* (optional) callback that tracks DOM changes */}
|
||||
onChangeClientState={(newState, addedTags, removedTags) => console.log(newState, addedTags, removedTags)}
|
||||
>
|
||||
{/* html attributes */}
|
||||
<html lang="en" amp />
|
||||
|
||||
{/* body attributes */}
|
||||
<body className="root" />
|
||||
|
||||
{/* title attributes and value */}
|
||||
<title itemProp="name" lang="en">My Plain Title or {`dynamic`} title</title>
|
||||
|
||||
{/* base element */}
|
||||
<base target="_blank" href="http://mysite.com/" />
|
||||
|
||||
{/* multiple meta elements */}
|
||||
<meta name="description" content="Helmet application" />
|
||||
<meta property="og:type" content="article" />
|
||||
|
||||
{/* multiple link elements */}
|
||||
<link rel="canonical" href="http://mysite.com/example" />
|
||||
<link rel="apple-touch-icon" href="http://mysite.com/img/apple-touch-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="http://mysite.com/img/apple-touch-icon-72x72.png" />
|
||||
{locales.map((locale) => {
|
||||
<link rel="alternate" href="http://example.com/{locale}" hrefLang={locale} key={locale}/>
|
||||
})}
|
||||
|
||||
{/* multiple script elements */}
|
||||
<script src="http://include.com/pathtojs.js" type="text/javascript" />
|
||||
|
||||
{/* inline script elements */}
|
||||
<script type="application/ld+json">{`
|
||||
{
|
||||
"@context": "http://schema.org"
|
||||
}
|
||||
`}</script>
|
||||
|
||||
{/* noscript elements */}
|
||||
<noscript>{`
|
||||
<link rel="stylesheet" type="text/css" href="foo.css" />
|
||||
`}</noscript>
|
||||
|
||||
{/* inline style elements */}
|
||||
<style type="text/css">{`
|
||||
body {
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
}
|
||||
`}</style>
|
||||
</Helmet>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
<img align="left" height="200" src="http://static.nfl.com/static/content/public/static/img/logos/ENG_SigilLockup_4C_POS_RGB.png" />
|
||||
107
webapi/ClientApp/src/components/ReactHelmet/Types.ts
Normal file
107
webapi/ClientApp/src/components/ReactHelmet/Types.ts
Normal file
@ -0,0 +1,107 @@
|
||||
// Type definitions for react-helmet 6.1
|
||||
// Project: https://github.com/nfl/react-helmet
|
||||
// Definitions by: Evan Bremer <https://github.com/evanbb>
|
||||
// Isman Usoh <https://github.com/isman-usoh>
|
||||
// Kok Sam <https://github.com/sammkj>
|
||||
// Yui T. <https://github.com/yuit>
|
||||
// Yamagishi Kazutoshi <https://github.com/ykzts>
|
||||
// Justin Hall <https://github.com/wKovacs64>
|
||||
// Andriy2 <https://github.com/Andriy2>
|
||||
// Piotr Błażejewicz <https://github.com/peterblazejewicz>
|
||||
// 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<HelmetProps["encodeSpecialCharacters"]>;
|
||||
};
|
||||
|
||||
declare class Helmet extends React.Component<HelmetProps> {
|
||||
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<HTMLBodyElement>;
|
||||
}
|
||||
|
||||
export interface HelmetHTMLElementDatum {
|
||||
toString(): string;
|
||||
toComponent(): React.HTMLAttributes<HTMLHtmlElement>;
|
||||
}
|
||||
|
||||
export let canUseDOM: boolean
|
||||
295
webapi/ClientApp/src/components/ReactHelmet/index.js
Normal file
295
webapi/ClientApp/src/components/ReactHelmet/index.js
Normal file
@ -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": "<img src='http://mysite.com/js/test.js'"}]
|
||||
* @param {Function} onChangeClientState: "(newState) => 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 <Helmet> 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
|
||||
}>{\`\`}</${
|
||||
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 <Component {...newProps} />
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
21
webapi/ClientApp/src/components/ReactSideEffect/LICENSE
Normal file
21
webapi/ClientApp/src/components/ReactSideEffect/LICENSE
Normal file
@ -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.
|
||||
140
webapi/ClientApp/src/components/ReactSideEffect/README.md
Normal file
140
webapi/ClientApp/src/components/ReactSideEffect/README.md
Normal file
@ -0,0 +1,140 @@
|
||||
# React Side Effect [](https://npmjs.com/react-side-effect) [](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
|
||||
<script src="https://unpkg.com/react/umd/react.development.js" type="text/javascript"></script>
|
||||
<script src="https://unpkg.com/react-side-effect/lib/index.umd.js" type="text/javascript"></script>
|
||||
```
|
||||
|
||||
#### Production
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/react/umd/react.production.min.js" type="text/javascript"></script>
|
||||
<script src="https://unpkg.com/react-side-effect/lib/index.umd.min.js" type="text/javascript"></script>
|
||||
```
|
||||
|
||||
## 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 `<BodyStyle style>` component like this:
|
||||
|
||||
```jsx
|
||||
// RootComponent.js
|
||||
return (
|
||||
<BodyStyle style={{ backgroundColor: 'red' }}>
|
||||
{this.state.something ? <SomeComponent /> : <OtherComponent />}
|
||||
</BodyStyle>
|
||||
);
|
||||
|
||||
// SomeComponent.js
|
||||
return (
|
||||
<BodyStyle style={{ backgroundColor: this.state.color }}>
|
||||
<div>Choose color: <input valueLink={this.linkState('color')} /></div>
|
||||
</BodyStyle>
|
||||
);
|
||||
```
|
||||
|
||||
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);
|
||||
```
|
||||
92
webapi/ClientApp/src/components/ReactSideEffect/index.js
Normal file
92
webapi/ClientApp/src/components/ReactSideEffect/index.js
Normal file
@ -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 <WrappedComponent {...this.props} />
|
||||
}
|
||||
}
|
||||
|
||||
return SideEffect
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
66
webapi/ClientApp/src/functions/dateTimeFormat.ts
Normal file
66
webapi/ClientApp/src/functions/dateTimeFormat.ts
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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 <header>
|
||||
<Navbar className="navbar-expand-sm navbar-toggleable-sm fixed-top border-bottom box-shadow mb-3 bg-light">
|
||||
<NavbarBrand href="/">{content?.siteName}</ NavbarBrand>
|
||||
@ -28,19 +35,13 @@ const NavMenu : FC = () => {
|
||||
<ul className="navbar-nav flex-grow">
|
||||
{content?.topMenu ? content.topMenu.map((item: MenuItemModel, index: number) => {
|
||||
return <NavItem key={index}>
|
||||
<NavLink tag={Link} className="text-dark" to={item.target}>
|
||||
{item.icon ? <><FeatherIcon icon={item.icon}/> </> : ''}{item.title}
|
||||
<NavLink tag={Link} className="text-dark" to={item.target}><>
|
||||
{item.icon ? <><FeatherIcon icon={item.icon}/> </> : ''}{titleFormatter(item.title)}</>
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
}) : ''}
|
||||
</ul>
|
||||
</Collapse>
|
||||
|
||||
{/* <form className="d-flex">
|
||||
<button className="btn btn-outline-dark" type="submit">
|
||||
<FeatherIcon icon='shopping-cart' className="me-1"/>Cart <span className="badge bg-dark text-white ms-1 rounded-pill">0</span>
|
||||
</button>
|
||||
</form> */}
|
||||
</Navbar>
|
||||
</header>
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import { PersonModel, PostItemModel } from "./abstractions"
|
||||
|
||||
|
||||
export interface PaginationModel<T> {
|
||||
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<T> {
|
||||
totalPages: number,
|
||||
currentPage: number,
|
||||
items: T []
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<number>(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 <Container fluid>
|
||||
<section className="pt-5 pb-5">
|
||||
<div className="row w-100">
|
||||
<div className="col-lg-12 col-md-12 col-12">
|
||||
<h3 className="display-5 mb-2 text-center">Shopping Cart</h3>
|
||||
<p className="mb-5 text-center">
|
||||
<i className="text-info font-weight-bold">3</i> items in your cart</p>
|
||||
<h3 className="display-5 mb-2 text-center">{titleSection.title}</h3>
|
||||
<p className="mb-5 text-center"><i className="text-info font-weight-bold">{shopCart?.items ? shopCart.items.length : 0}</i> {titleSection.text}</p>
|
||||
<table id="shoppingCart" className="table table-condensed table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: "60%" }}>Product</th>
|
||||
<th style={{ width: "12%" }}>Price</th>
|
||||
<th style={{ width: "10%" }}>Quantity</th>
|
||||
<th style={{ width: "60%" }}>{productsSection.product}</th>
|
||||
<th style={{ width: "12%" }}>{productsSection.price}</th>
|
||||
<th style={{ width: "10%" }}>{productsSection.quantity}</th>
|
||||
<th style={{ width: "16%" }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{[{}, {}, {}, {}].map((item, index) => <tr key={index}>
|
||||
{(shopCart?.items ? shopCart.items : []).map((item, index) => <tr key={index}>
|
||||
<td data-th="Product">
|
||||
<div className="row">
|
||||
<div className="col-md-3 text-left">
|
||||
<img src="https://dummyimage.com/250x250/ced4da/6c757d.jpg" alt="" className="img-fluid d-none d-md-block rounded mb-2 shadow" />
|
||||
</div>
|
||||
<div className="col-md-9 text-left mt-sm-2">
|
||||
<h4>Product Name</h4>
|
||||
<p className="font-weight-light">Brand & Name</p>
|
||||
<h4>{item.title}</h4>
|
||||
<p className="font-weight-light">{item.brandName}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td data-th="Price">$49.00</td>
|
||||
<td data-th="Price">{item.newPrice
|
||||
? <><span className="text-muted text-decoration-line-through">{currencySymbol}{item.price.toFixed(2)}</span> <span>{currencySymbol}{item.newPrice.toFixed(2)}</span></>
|
||||
: <span>{currencySymbol}{item.price.toFixed(2)}</span>}</td>
|
||||
<td data-th="Quantity">
|
||||
<input type="number" className="form-control form-control-lg text-center" value="1" />
|
||||
<input type="number" className="form-control form-control-lg text-center" value={item.quantity} />
|
||||
</td>
|
||||
<td className="actions" data-th="">
|
||||
<div className="text-right">
|
||||
<button className="btn btn-white border-secondary bg-white btn-md mb-2">
|
||||
<FeatherIcon icon="refresh-cw" />
|
||||
</button>
|
||||
<button className="btn btn-white border-secondary bg-white btn-md mb-2">
|
||||
<FeatherIcon icon="trash-2" />
|
||||
</button>
|
||||
@ -55,18 +103,18 @@ const Cart = () => {
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="float-right text-right">
|
||||
<h4>Subtotal:</h4>
|
||||
<h1>$99.00</h1>
|
||||
<h4>{productsSection.subtotal}</h4>
|
||||
<h1>{currencySymbol}{subtotal}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row mt-4 d-flex align-items-center">
|
||||
<div className="col-sm-6 order-md-2 text-right">
|
||||
<a href="catalog.html" className="btn btn-primary mb-4 btn-lg pl-5 pr-5">Checkout</a>
|
||||
<a href="catalog.html" className="btn btn-primary mb-4 btn-lg pl-5 pr-5">{productsSection.submit.title}</a>
|
||||
</div>
|
||||
<div className="col-sm-6 mb-3 mb-m-1 order-md-1 text-md-left">
|
||||
<a href="catalog.html">
|
||||
<i className="fas fa-arrow-left mr-2"></i> Continue Shopping</a>
|
||||
<a href={productsSection.continueShopping.target}>
|
||||
<FeatherIcon icon="arrow-left" /> {productsSection.continueShopping.anchorText}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -36,13 +36,17 @@ const TitleSection: FC<TitleSectionModel> = ({
|
||||
|
||||
|
||||
interface ShopItems {
|
||||
path?: string
|
||||
currencySymbol: string,
|
||||
addToCart: string,
|
||||
path: string
|
||||
totalPages?: number,
|
||||
currentPage?: number,
|
||||
items?: ShopItemModel []
|
||||
}
|
||||
|
||||
const ShopItemsSection: FC<ShopItems> = ({
|
||||
currencySymbol = "",
|
||||
addToCart = "",
|
||||
path = "",
|
||||
totalPages = 1,
|
||||
currentPage = 1,
|
||||
@ -57,7 +61,9 @@ const ShopItemsSection: FC<ShopItems> = ({
|
||||
<Row className="gx-4 gx-lg-5 row-cols-2 row-cols-md-3 row-cols-xl-4 justify-content-center">
|
||||
{items.map((item, index) => <Col key={index} className="mb-5">
|
||||
<Card className="h-100">
|
||||
<div className="badge bg-dark text-white position-absolute" style={{top: "0.5rem", right: "0.5rem"}}>{item.badges}</div>
|
||||
<div className="position-absolute" style={{top: "0.5rem", right: "0.5rem"}}>
|
||||
{(item?.badges ? item.badges : []).map((badge, index) => <div key={index} className="badge bg-dark text-white" style={{marginLeft: "0.5rem"}}>{badge}</div>) }
|
||||
</div>
|
||||
|
||||
<Link to={`${path}/${currentPage}/${item.slug}`}>
|
||||
<CardImg top {...item.image} />
|
||||
@ -72,18 +78,15 @@ const ShopItemsSection: FC<ShopItems> = ({
|
||||
}} />
|
||||
|
||||
{item.newPrice
|
||||
? <><span className="text-muted text-decoration-line-through">{item.price}</span> <span>{item.newPrice}</span></>
|
||||
: <span>{item.price}</span>}
|
||||
|
||||
? <><span className="text-muted text-decoration-line-through">{currencySymbol}{item.price.toFixed(2)}</span> <span>{currencySymbol}{item.newPrice.toFixed(2)}</span></>
|
||||
: <span>{currencySymbol}{item.price.toFixed(2)}</span>}
|
||||
</div>
|
||||
</CardBody>
|
||||
<CardFooter className="p-4 pt-0 border-top-0 bg-transparent">
|
||||
<div className="text-center"><a className="btn btn-outline-dark mt-auto" href="#">Add to cart</a></div>
|
||||
<div className="text-center"><a className="btn btn-outline-dark mt-auto" href="#">{addToCart}</a></div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Col>)}
|
||||
|
||||
|
||||
</Row>
|
||||
|
||||
<Pagination {...{
|
||||
@ -110,6 +113,10 @@ const ShopCatalog = () => {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -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 <>
|
||||
<section className="py-5">
|
||||
@ -43,16 +60,19 @@ const ShopItem = () => {
|
||||
<h1 className="display-5 fw-bolder">{shopItem?.title}</h1>
|
||||
<div className="fs-5 mb-5">
|
||||
{shopItem?.newPrice
|
||||
? <><span className="text-decoration-line-through">{shopItem?.price}</span> <span>{shopItem?.newPrice}</span></>
|
||||
: <span>{shopItem?.price}</span>}
|
||||
? <><span className="text-muted text-decoration-line-through">{currencySymbol}{shopItem.price.toFixed(2)}</span> <span>{currencySymbol}{shopItem.newPrice.toFixed(2)}</span></>
|
||||
: <span>{currencySymbol}{shopItem?.price.toFixed(2)}</span>}
|
||||
</div>
|
||||
|
||||
<section dangerouslySetInnerHTML={{ __html: shopItem?.text ? shopItem.text : '' }}></section>
|
||||
|
||||
<div className="d-flex pb-1">{productSection.availableQuantity} {shopItem?.quantity ? shopItem.quantity : 0}</div>
|
||||
|
||||
<div className="d-flex">
|
||||
<input className="form-control text-center me-3" id="inputQuantity" type="num" value="1" style={{maxWidth: "3rem"}} />
|
||||
|
||||
<button className="btn btn-outline-dark flex-shrink-0" type="button">
|
||||
<FeatherIcon icon='shopping-cart' className="me-1"/> Add to cart</button>
|
||||
<FeatherIcon icon='shopping-cart' className="me-1"/> {productSection.addToCart}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 <section className="py-5 bg-light">
|
||||
<Container fluid className="px-4 px-lg-5 my-5">
|
||||
<h2 className="fw-bolder mb-4">Related products</h2>
|
||||
<Container fluid className="px-4 px-lg-5">
|
||||
<h2 className="fw-bolder mb-4">{relatedProductsSection.title}</h2>
|
||||
|
||||
<Row className="gx-4 gx-lg-5 row-cols-2 row-cols-md-3 row-cols-xl-4 justify-content-center">
|
||||
{items.map((item, index) => <Col key={index} className="mb-5">
|
||||
<Card className="h-100">
|
||||
<div className="badge bg-dark text-white position-absolute" style={{top: "0.5rem", right: "0.5rem"}}>Sale</div>
|
||||
<CardImg className="card-img-top" src="https://dummyimage.com/450x300/dee2e6/6c757d.jpg" alt="..." />
|
||||
<div className="position-absolute" style={{top: "0.5rem", right: "0.5rem"}}>
|
||||
{(item?.badges ? item.badges : []).map((badge, index) => <div key={index} className="badge bg-dark text-white" style={{marginLeft: "0.5rem"}}>{badge}</div>) }
|
||||
</div>
|
||||
<CardImg className="card-img-top" {...item.image} />
|
||||
<CardBody className="p-4">
|
||||
<div className="text-center">
|
||||
|
||||
<h5 className="fw-bolder">Special Item</h5>
|
||||
|
||||
<div className="d-flex justify-content-center small text-warning mb-2">
|
||||
<div className="bi-star-fill"></div>
|
||||
<div className="bi-star-fill"></div>
|
||||
<div className="bi-star-fill"></div>
|
||||
<div className="bi-star-fill"></div>
|
||||
<div className="bi-star-fill"></div>
|
||||
</div>
|
||||
|
||||
<span className="text-muted text-decoration-line-through">$20.00</span>
|
||||
$18.00
|
||||
<h5 className="fw-bolder">{item.title}</h5>
|
||||
<FeatherRating {...{
|
||||
value: item?.rating ? item.rating : 0
|
||||
}} />
|
||||
|
||||
{item.newPrice
|
||||
? <><span className="text-muted text-decoration-line-through">{currencySymbol}{item.price.toFixed(2)}</span> <span>{currencySymbol}{item.newPrice.toFixed(2)}</span></>
|
||||
: <span>{currencySymbol}{item.price.toFixed(2)}</span>}
|
||||
</div>
|
||||
</CardBody>
|
||||
<CardFooter className="p-4 pt-0 border-top-0 bg-transparent">
|
||||
<div className="text-center"><a className="btn btn-outline-dark mt-auto" href="#">Add to cart</a></div>
|
||||
<div className="text-center"><a className="btn btn-outline-dark mt-auto" href="#">{relatedProductsSection.addToCart}</a></div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Col>)}
|
||||
</Row>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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<KnownAction> => (dispatch, getState) => {
|
||||
|
||||
},
|
||||
|
||||
addToCart: (): AppThunkAction<KnownAction> => (dispatch, getState) => {
|
||||
|
||||
// Get<Promise<GetBlogItemResponseModel>>('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<KnownAction> => (dispatch, getState) => {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const unloadedState: CartState = {
|
||||
quantity: 0,
|
||||
|
||||
isLoading: false
|
||||
}
|
||||
|
||||
export const reducer: Reducer<CartState> = (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
|
||||
}
|
||||
@ -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."
|
||||
|
||||
148
webapi/ClientApp/src/store/reducers/ShopCart.ts
Normal file
148
webapi/ClientApp/src/store/reducers/ShopCart.ts
Normal file
@ -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<KnownAction> => (dispatch, getState) => {
|
||||
|
||||
},
|
||||
|
||||
addToCart: (): AppThunkAction<KnownAction> => (dispatch, getState) => {
|
||||
|
||||
// Get<Promise<GetBlogItemResponseModel>>('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<KnownAction> => (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<ShopCartState> = (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
|
||||
}
|
||||
@ -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: {
|
||||
|
||||
@ -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: "",
|
||||
|
||||
@ -49,6 +49,8 @@ const unloadedState: ShopItemState = {
|
||||
],
|
||||
|
||||
title: "Shop item template",
|
||||
brandName: "Brand & Name",
|
||||
|
||||
text: `<p className="lead">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?</p>`,
|
||||
author: {
|
||||
id: "",
|
||||
|
||||
@ -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<KnownAction> => (dispatch, getState) => {
|
||||
requestShopRelated: (props?: GetShopRelatedRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
|
||||
|
||||
Get<Promise<GetShopCatalogResponseModel>>('https://localhost:7151/api/ShopRelated', props)
|
||||
Get<Promise<GetShopRelatedResponseModel>>('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: {
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
6
webapi/WeatherForecast/Models/LinkModel.cs
Normal file
6
webapi/WeatherForecast/Models/LinkModel.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace WeatherForecast.Models {
|
||||
public class LinkModel {
|
||||
public string Target { get; set; }
|
||||
public string AnchorText { get; set; }
|
||||
}
|
||||
}
|
||||
15
webapi/WeatherForecast/Models/LocalizationModel.cs
Normal file
15
webapi/WeatherForecast/Models/LocalizationModel.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user