(feat): react helmet

This commit is contained in:
Maksym Sadovnychyy 2022-07-10 00:14:09 +02:00
parent 25da28f001
commit 901b7f02e3
39 changed files with 2759 additions and 227 deletions

View File

@ -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') : ''}

View 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.

View 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.
![benchmark chart](assets/benchmarking.png "benchmarking chart")
(Check out the [benchmarking details](#benchmarking-this-library).)
## Install
```sh
$ yarn add react-fast-compare
# or
$ npm install react-fast-compare
```
## Highlights
- ES5 compatible; works in node.js (0.10+) and browsers (IE9+)
- deeply compares any value (besides objects with circular references)
- handles React-specific circular references, like elements
- checks equality Date and RegExp objects
- should as fast as [fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal) via a single unified library, and with added guardrails for circular references.
- small: under 650 bytes minified+gzipped
## Usage
```jsx
const isEqual = require("react-fast-compare");
// general usage
console.log(isEqual({ foo: "bar" }, { foo: "bar" })); // true
// React.memo
// only re-render ExpensiveComponent when the props have deeply changed
const DeepMemoComponent = React.memo(ExpensiveComponent, isEqual);
// React.Component shouldComponentUpdate
// only re-render AnotherExpensiveComponent when the props have deeply changed
class AnotherExpensiveComponent extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
render() {
// ...
}
}
```
## Do I Need `React.memo` (or `shouldComponentUpdate`)?
> What's faster than a really fast deep comparison? No deep comparison at all.
—This Readme
Deep checks in `React.memo` or a `shouldComponentUpdate` should not be used blindly.
First, see if the default
[React.memo](https://reactjs.org/docs/react-api.html#reactmemo) or
[PureComponent](https://reactjs.org/docs/react-api.html#reactpurecomponent)
will work for you. If it won't (if you need deep checks), it's wise to make
sure you've correctly indentified the bottleneck in your application by
[profiling the performance](https://reactjs.org/docs/optimizing-performance.html#profiling-components-with-the-chrome-performance-tab).
After you've determined that you _do_ need deep equality checks and you've
identified the minimum number of places to apply them, then this library may
be for you!
## Benchmarking this Library
The absolute values are much less important than the relative differences
between packages.
Benchmarking source can be found
[here](https://github.com/FormidableLabs/react-fast-compare/blob/master/benchmark/index.js).
Each "operation" consists of running all relevant tests. The React benchmark
uses both the generic tests and the react tests; these runs will be slower
simply because there are more tests in each operation.
The results below are from a local test on a laptop.
### Generic Data
```
react-fast-compare x 157,863 ops/sec ±0.54% (94 runs sampled)
fast-deep-equal x 149,877 ops/sec ±0.76% (93 runs sampled)
lodash.isEqual x 33,298 ops/sec ±0.70% (93 runs sampled)
nano-equal x 144,836 ops/sec ±0.51% (94 runs sampled)
shallow-equal-fuzzy x 110,192 ops/sec ±0.57% (95 runs sampled)
fastest: react-fast-compare
```
`react-fast-compare` and `fast-deep-equal` should be the same speed for these
tests; any difference is just noise. `react-fast-compare` won't be faster than
`fast-deep-equal`, because it's based on it.
### React and Generic Data
```
react-fast-compare x 64,102 ops/sec ±0.36% (94 runs sampled)
fast-deep-equal x 63,844 ops/sec ±0.43% (94 runs sampled)
lodash.isEqual x 6,243 ops/sec ±0.72% (90 runs sampled)
fastest: react-fast-compare,fast-deep-equal
```
Two of these packages cannot handle comparing React elements, because they
contain circular reference: `nano-equal` and `shallow-equal-fuzzy`.
### Running Benchmarks
```sh
$ yarn install
$ yarn run benchmark
```
## Differences between this library and `fast-deep-equal`
`react-fast-compare` is based on `fast-deep-equal`, with some additions:
- `react-fast-compare` has `try`/`catch` guardrails for stack overflows from undetected (non-React) circular references.
- `react-fast-compare` has a _single_ unified entry point for all uses. No matter what your target application is, `import equal from 'react-fast-compare'` just works. `fast-deep-equal` has multiple entry points for different use cases.
This version of `react-fast-compare` tracks `fast-deep-equal@3.1.1`.
## Bundle Size
There are a variety of ways to calculate bundle size for JavaScript code.
You can see our size test code in the `compress` script in
[`package.json`](https://github.com/FormidableLabs/react-fast-compare/blob/master/package.json).
[Bundlephobia's calculation](https://bundlephobia.com/result?p=react-fast-compare) is slightly higher,
as they [do not mangle during minification](https://github.com/pastelsky/package-build-stats/blob/v6.1.1/src/getDependencySizeTree.js#L139).
## License
[MIT](https://github.com/FormidableLabs/react-fast-compare/blob/readme/LICENSE)
## Contributing
Please see our [contributions guide](./CONTRIBUTING.md).
## Maintenance Status
**Active:** Formidable is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome.
[trav_img]: https://api.travis-ci.com/FormidableLabs/react-fast-compare.svg
[trav_site]: https://travis-ci.com/FormidableLabs/react-fast-compare
[cov_img]: https://img.shields.io/coveralls/FormidableLabs/react-fast-compare.svg
[cov_site]: https://coveralls.io/r/FormidableLabs/react-fast-compare
[npm_img]: https://badge.fury.io/js/react-fast-compare.svg
[npm_site]: http://badge.fury.io/js/react-fast-compare
[appveyor_img]: https://ci.appveyor.com/api/projects/status/github/formidablelabs/react-fast-compare?branch=master&svg=true
[appveyor_site]: https://ci.appveyor.com/project/FormidableLabs/react-fast-compare
[bundle_img]: https://img.shields.io/badge/minzipped%20size-622%20B-flatgreen.svg
[downloads_img]: https://img.shields.io/npm/dm/react-fast-compare.svg
[maintenance_img]: https://img.shields.io/badge/maintenance-active-flatgreen.svg

View 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

View File

@ -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'

View 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
}
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 }

View 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.

View 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
[![npm Version](https://img.shields.io/npm/v/react-helmet.svg?style=flat-square)](https://www.npmjs.org/package/react-helmet)
[![codecov.io](https://img.shields.io/codecov/c/github/nfl/react-helmet.svg?branch=master&style=flat-square)](https://codecov.io/github/nfl/react-helmet?branch=master)
[![Build Status](https://img.shields.io/travis/nfl/react-helmet/master.svg?style=flat-square)](https://travis-ci.org/nfl/react-helmet)
[![Dependency Status](https://img.shields.io/david/nfl/react-helmet.svg?style=flat-square)](https://david-dm.org/nfl/react-helmet)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](CONTRIBUTING.md#pull-requests)
This reusable React component will manage all of your changes to the document head.
Helmet _takes_ plain HTML tags and _outputs_ plain HTML tags. It's dead simple, and React beginner friendly.
## [6.0.0-beta Release Notes](https://github.com/nfl/react-helmet/wiki/Upgrade-from-5.x.x----6.x.x-beta)
## Example
```javascript
import React from "react";
import {Helmet} from "react-helmet";
class Application extends React.Component {
render () {
return (
<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" />

View 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

View 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.
// Dont 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

View 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.

View File

@ -0,0 +1,140 @@
# React Side Effect [![Downloads](https://img.shields.io/npm/dm/react-side-effect.svg)](https://npmjs.com/react-side-effect) [![npm version](https://img.shields.io/npm/v/react-side-effect.svg?style=flat)](https://www.npmjs.com/package/react-side-effect)
Create components whose prop changes map to a global side effect.
## Installation
```
npm install --save react-side-effect
```
### As a script tag
#### Development
```html
<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, youll 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. Dont 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);
```

View 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
}
}

View File

@ -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
}

View 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
}

View File

@ -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
}

View File

@ -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>
}

View File

@ -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,

View File

@ -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 []
}

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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 &amp; 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>

View File

@ -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
}

View File

@ -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>

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 Bootstraps form controls. Each required form group has a validation state that can be triggered by attempting to submit the form without completing it."

View 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
}

View File

@ -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: {

View File

@ -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: "",

View File

@ -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: "",

View File

@ -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: {

View File

@ -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; }
}
}

View File

@ -0,0 +1,6 @@
namespace WeatherForecast.Models {
public class LinkModel {
public string Target { get; set; }
public string AnchorText { get; set; }
}
}

View 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; }
}
}