From 8c592b269ecc1f30a22dc5698074e5cd046e85b1 Mon Sep 17 00:00:00 2001 From: "Erin E. Sullivan" Date: Wed, 30 Aug 2023 14:28:34 -0400 Subject: [PATCH 1/2] Adding `ChooseAffiliation` component above `InstitutionSelect`. - Updating `FlintAlerts` --- package-lock.json | 44 +--- package.json | 1 - .../components/ChooseAffiliation/index.js | 65 ++++++ .../components/choose-affiliation.js | 201 ------------------ src/modules/affiliation/index.js | 8 +- src/modules/affiliation/reducer/index.js | 16 +- .../core/components/SearchHeader/index.js | 3 - .../components/FlintAlerts/index.js | 61 ++++++ src/modules/datastores/index.js | 4 +- .../flint/components/FlintAlerts/index.js | 60 ------ .../components/UserIsFlintAffiliated/index.js | 30 --- src/modules/flint/index.js | 7 - .../components/DatastorePage/container.js | 22 +- .../components/URLSearchQueryWrapper/index.js | 3 +- .../reusable/components/Dialog/index.js | 91 -------- src/modules/reusable/index.js | 2 - src/stylesheets/main.css | 1 - 17 files changed, 146 insertions(+), 473 deletions(-) create mode 100644 src/modules/affiliation/components/ChooseAffiliation/index.js delete mode 100644 src/modules/affiliation/components/choose-affiliation.js create mode 100644 src/modules/datastores/components/FlintAlerts/index.js delete mode 100644 src/modules/flint/components/FlintAlerts/index.js delete mode 100644 src/modules/flint/components/UserIsFlintAffiliated/index.js delete mode 100644 src/modules/flint/index.js delete mode 100644 src/modules/reusable/components/Dialog/index.js diff --git a/package-lock.json b/package-lock.json index 9c1bb3a7a..7905ceee1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "qs": "^6.11.2", "react": "^18.2.0", "react-autosuggest": "^10.1.0", - "react-cookie": "^4.1.1", "react-dom": "^18.2.0", "react-ga": "^3.3.1", "react-modal": "^3.16.1", @@ -3786,11 +3785,6 @@ "@types/node": "*" } }, - "node_modules/@types/cookie": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", - "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" - }, "node_modules/@types/eslint": { "version": "8.44.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", @@ -6153,14 +6147,6 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, - "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -13986,19 +13972,6 @@ "react": ">=16.3.0" } }, - "node_modules/react-cookie": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz", - "integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.0.1", - "hoist-non-react-statics": "^3.0.0", - "universal-cookie": "^4.0.0" - }, - "peerDependencies": { - "react": ">= 16.3.0" - } - }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -16271,9 +16244,9 @@ } }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "peer": true, "bin": { @@ -16281,7 +16254,7 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { @@ -16356,15 +16329,6 @@ "node": ">=8" } }, - "node_modules/universal-cookie": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", - "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", - "dependencies": { - "@types/cookie": "^0.3.3", - "cookie": "^0.4.0" - } - }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", diff --git a/package.json b/package.json index 6f5cc743d..deff2bf27 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "qs": "^6.11.2", "react": "^18.2.0", "react-autosuggest": "^10.1.0", - "react-cookie": "^4.1.1", "react-dom": "^18.2.0", "react-ga": "^3.3.1", "react-modal": "^3.16.1", diff --git a/src/modules/affiliation/components/ChooseAffiliation/index.js b/src/modules/affiliation/components/ChooseAffiliation/index.js new file mode 100644 index 000000000..c1e118928 --- /dev/null +++ b/src/modules/affiliation/components/ChooseAffiliation/index.js @@ -0,0 +1,65 @@ +import { React, useState } from 'react'; + +const initialAffiliationState = { + active: localStorage.getItem('affiliation') || undefined, + defaultAffiliation: 'aa', + affiliationOptions: { + aa: 'Ann Arbor', + flint: 'Flint' + } +}; + +function ChooseAffiliation () { + let activeAffiliation = initialAffiliationState.defaultAffiliation; + // Set default affiliation if query parameter exists + const urlParams = new URLSearchParams(window.location.search); + const affiliationParam = urlParams.get('affiliation'); + if (affiliationParam) { + activeAffiliation = affiliationParam; + // Set localStorage + localStorage.setItem('affiliation', affiliationParam); + } + const affiliationLocalStorage = localStorage.getItem('affiliation'); + // Set default affiliation if localStorage exists + if (affiliationLocalStorage) { + activeAffiliation = affiliationLocalStorage; + } + const [affiliation, setAffiliation] = useState(activeAffiliation); + const onAffiliationChange = (event) => { + const value = event.target.value; + setAffiliation(value); + urlParams.set('affiliation', value); + window.location.href = `${window.location.origin}${window.location.pathname}?${urlParams.toString()}`; + }; + + return ( +
+ Choose Affiliation + + +

Selecting an affiliation helps us connect you to available online materials licensed for your campus. You can still use Library Search if you're not affiliated with either campus.

+
+ ); +} + +export { initialAffiliationState, ChooseAffiliation }; diff --git a/src/modules/affiliation/components/choose-affiliation.js b/src/modules/affiliation/components/choose-affiliation.js deleted file mode 100644 index 278be951e..000000000 --- a/src/modules/affiliation/components/choose-affiliation.js +++ /dev/null @@ -1,201 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import React, { useState } from 'react'; -import { useCookies } from 'react-cookie'; -import { useSelector } from 'react-redux'; -import qs from 'qs'; -import { COLORS } from '../../reusable/umich-lib-core-temp'; -import { Button, Dialog } from '../../reusable'; - -export default function ChooseAffiliation () { - const { defaultAffiliation, affiliationOptions } = useSelector((state) => { - return state.affiliation; - }); - const [cookies] = useCookies(['affiliation']); - let affiliation = cookies.affiliation || defaultAffiliation; - const urlParams = new URLSearchParams(window.location.search); - affiliation = urlParams.get('affiliation') || affiliation; - const alternativeAffiliation = affiliation === 'flint' ? 'aa' : 'flint'; - const changeAffiliation = () => { - const parsed = qs.parse(document.location.search.substring(1), { - allowDots: true - }); - const withAffiliation = { - ...parsed, - affiliation: alternativeAffiliation - }; - document.location.href = - document.location.pathname + - '?' + - qs.stringify(withAffiliation, { - arrayFormat: 'repeat', - encodeValuesOnly: true, - allowDots: true, - format: 'RFC1738' - }); - }; - const [dialogOpen, setDialogOpen] = useState(false); - const toggleDialog = () => { - return setDialogOpen((bool) => { - return !bool; - }); - }; - const closeDialog = () => { - return setDialogOpen(false); - }; - /* - Safari <15.4 does not support the `close()` method used for the native HTML5 element. - If the user is on an unsupported version of Safari, display just buttons. - */ - const userAgent = navigator.userAgent; - let oldSafari = false; - if ([' Version/', ' Safari/'].every((spec) => { - return userAgent.includes(spec); - })) { - // Split userAgent up at `Version/` - const agentSplit = userAgent.split('Version/'); - // Split the second element of the new array - const version = agentSplit[1].split(' '); - // Turn the first element of the array into an integer, and compare it to 15.4 - oldSafari = parseFloat(version[0]) < 15.4; - } - if (oldSafari) { - return ( -
- -
- ); - } - return ( -
- - -
-
h2': { - marginTop: '0' - }, - '& > *:last-child': { - marginBottom: '0' - } - }} - > -

- Choose campus affiliation -

-

- Selecting an affiliation helps us connect you to available online - materials licensed for your campus. -

-
- - or - -
-

- You can still use Library Search if you're not affiliated with - either campus. -

-
- -
-
-
- ); -} diff --git a/src/modules/affiliation/index.js b/src/modules/affiliation/index.js index 4070f875b..7c54ac2c0 100644 --- a/src/modules/affiliation/index.js +++ b/src/modules/affiliation/index.js @@ -1,13 +1,7 @@ -import ChooseAffiliation from './components/choose-affiliation'; +import { ChooseAffiliation } from './components/ChooseAffiliation'; import affiliationReducer from './reducer'; import { setDefaultAffiliation, setActiveAffilitation } from './actions'; -export function affiliationCookieSetter (affiliation) { - if (affiliation) { - document.cookie = `affiliation=${affiliation};path=/`; - } -} - export { ChooseAffiliation, affiliationReducer, diff --git a/src/modules/affiliation/reducer/index.js b/src/modules/affiliation/reducer/index.js index 97ac77a38..22162526a 100644 --- a/src/modules/affiliation/reducer/index.js +++ b/src/modules/affiliation/reducer/index.js @@ -1,25 +1,17 @@ +import { initialAffiliationState } from '../components/ChooseAffiliation'; import * as actions from '../actions/'; -const initialState = { - active: undefined, - defaultAffiliation: 'aa', - affiliationOptions: { - aa: 'Ann Arbor', - flint: 'Flint' - } -}; - -const affiliationReducer = (state = initialState, action) => { +const affiliationReducer = (state = initialAffiliationState, action) => { switch (action.type) { case actions.SET_DEFAULT_AFFILIATION: return { ...state, - defaultAffiliation: action.payload + defaultAffiliation: action.payload || initialAffiliationState.defaultAffiliation }; case actions.SET_ACTIVE_AFFILIATION: return { ...state, - active: action.payload + active: action.payload || initialAffiliationState.active }; default: return state; diff --git a/src/modules/core/components/SearchHeader/index.js b/src/modules/core/components/SearchHeader/index.js index 4d3ced124..462fa2133 100644 --- a/src/modules/core/components/SearchHeader/index.js +++ b/src/modules/core/components/SearchHeader/index.js @@ -2,8 +2,6 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; - -import { ChooseAffiliation } from '../../../affiliation'; import { MEDIA_QUERIES } from '../../../reusable/umich-lib-core-temp'; import { Authentication } from '../../../profile'; @@ -44,7 +42,6 @@ function SearchHeader (props) { > Account - ); diff --git a/src/modules/datastores/components/FlintAlerts/index.js b/src/modules/datastores/components/FlintAlerts/index.js new file mode 100644 index 000000000..a37afd047 --- /dev/null +++ b/src/modules/datastores/components/FlintAlerts/index.js @@ -0,0 +1,61 @@ +/** @jsxImportSource @emotion/react */ +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + +const alertMessages = { + primo: 'U-M Flint users: You may not be able to access U-M Ann Arbor resources. For the best results use Thompson Library’s Search All to search for articles.', + databases: 'We noticed you are affiliated with U-M Flint. For the best results use the Thompson Library’s database listing.', + onlinejournals: 'We noticed you are affiliated with U-M Flint. For the best results use the Thompson Library’s Search All to search for articles.', + website: 'We noticed you are affiliated with U-M Flint. For the best results use the Thompson Library website.' +}; + +function FlintAlerts (props) { + const [closed, setClosed] = useState(false); + // Check if user is not affiliated with Flint, is not in one of the above datastores, or the Alert is closed + if ( + !props.profile.institutions?.includes('Flint') || + !Object.keys(alertMessages).includes(props.datastore) || + closed + ) { + return null; + } + + const onClose = () => { + setClosed(true); + }; + + return ( +
+ + +
+ ); +}; + +FlintAlerts.propTypes = { + profile: PropTypes.object, + datastore: PropTypes.string +}; + +function mapStateToProps (state) { + return { + query: state.search.query, + profile: state.profile, + datastore: state.datastores.active + }; +} + +export default connect(mapStateToProps)(FlintAlerts); diff --git a/src/modules/datastores/index.js b/src/modules/datastores/index.js index 67cc7cf7a..634aa22bd 100644 --- a/src/modules/datastores/index.js +++ b/src/modules/datastores/index.js @@ -2,6 +2,7 @@ import DatastoreNavigation from './components/DatastoreNavigation'; import Landing from './components/Landing'; import DatastoreInfo from './components/DatastoreInfo'; import DatastoreAlerts from './components/DatastoreAlerts'; +import FlintAlerts from './components/FlintAlerts'; import datastoresReducer from './reducer/'; import { addDatastore, changeActiveDatastore } from './actions'; @@ -13,5 +14,6 @@ export { changeActiveDatastore, Landing, DatastoreInfo, - DatastoreAlerts + DatastoreAlerts, + FlintAlerts }; diff --git a/src/modules/flint/components/FlintAlerts/index.js b/src/modules/flint/components/FlintAlerts/index.js deleted file mode 100644 index e0676a540..000000000 --- a/src/modules/flint/components/FlintAlerts/index.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { Alert, Button } from '../../../reusable'; -import { connect } from 'react-redux'; -import { UserIsFlintAffiliated } from '../../../flint'; -import PropTypes from 'prop-types'; - -class FlintAlerts extends React.Component { - state = { - closed: false - }; - - handleCloseButtonClick = () => { - this.setState({ closed: true }); - }; - - render () { - const { datastore } = this.props; - - const alertMessages = { - primo: 'U-M Flint users: You may not be able to access U-M Ann Arbor resources. For the best results use Thompson Library’s Search All to search for articles.', - databases: 'We noticed you are affiliated with U-M Flint. For the best results use the Thompson Library’s database listing.', - onlinejournals: 'We noticed you are affiliated with U-M Flint. For the best results use the Thompson Library’s Search All to search for articles.', - website: 'We noticed you are affiliated with U-M Flint. For the best results use the Thompson Library website.' - }; - - if (this.state.closed || !Object.keys(alertMessages).includes(datastore)) { - return null; - } - - return ( - - - - - - - ); - } -} - -FlintAlerts.propTypes = { - datastore: PropTypes.string -}; - -function mapStateToProps (state) { - return { - query: state.search.query, - datastore: state.datastores.active - }; -} - -export default connect(mapStateToProps)(FlintAlerts); diff --git a/src/modules/flint/components/UserIsFlintAffiliated/index.js b/src/modules/flint/components/UserIsFlintAffiliated/index.js deleted file mode 100644 index ff4b3e4e0..000000000 --- a/src/modules/flint/components/UserIsFlintAffiliated/index.js +++ /dev/null @@ -1,30 +0,0 @@ -import _ from 'underscore'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; - -const UserIsFlintAffiliated = ({ - isFlintAffiliated, - children -}) => { - if (isFlintAffiliated) { - return children; - } else { - return null; - } -}; - -UserIsFlintAffiliated.propTypes = { - isFlintAffiliated: PropTypes.bool, - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node - ]) -}; - -function mapStateToProps (state) { - return { - isFlintAffiliated: _.contains(state.profile.institutions, 'Flint') - }; -} - -export default connect(mapStateToProps)(UserIsFlintAffiliated); diff --git a/src/modules/flint/index.js b/src/modules/flint/index.js deleted file mode 100644 index 94cd479d6..000000000 --- a/src/modules/flint/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import FlintAlerts from './components/FlintAlerts'; -import UserIsFlintAffiliated from './components/UserIsFlintAffiliated'; - -export { - FlintAlerts, - UserIsFlintAffiliated -}; diff --git a/src/modules/pages/components/DatastorePage/container.js b/src/modules/pages/components/DatastorePage/container.js index 6f2f7645b..3895826a4 100644 --- a/src/modules/pages/components/DatastorePage/container.js +++ b/src/modules/pages/components/DatastorePage/container.js @@ -9,6 +9,7 @@ import { AdvancedSearch } from '../../../advanced'; import { DatastoreNavigation, DatastoreInfo, + FlintAlerts, Landing } from '../../../datastores'; import { Filters } from '../../../filters'; @@ -22,9 +23,9 @@ import { import { GetThisPage } from '../../../getthis'; import { switchPrideToDatastore } from '../../../pride'; import { InstitutionSelect, InstitutionWrapper } from '../../../institution'; +import { ChooseAffiliation } from '../../../affiliation'; import { List } from '../../../lists'; import { setDocumentTitle } from '../../../a11y'; -import { FlintAlerts } from '../../../flint'; import PropTypes from 'prop-types'; import { Icon } from '../../../reusable'; @@ -101,20 +102,7 @@ class DatastorePageContainer extends React.Component { <> -
- -
+ { display: responsive('none', 'block') }} css={{ + '& > * + *': { + marginTop: '1rem' + }, '@media (max-width: 979px)': { display: 'none' }, @@ -263,6 +254,7 @@ const Results = ({ activeDatastore, activeFilterCount }) => { } }} > + diff --git a/src/modules/pride/components/URLSearchQueryWrapper/index.js b/src/modules/pride/components/URLSearchQueryWrapper/index.js index a0d8c50a1..9fd981dbb 100644 --- a/src/modules/pride/components/URLSearchQueryWrapper/index.js +++ b/src/modules/pride/components/URLSearchQueryWrapper/index.js @@ -27,7 +27,7 @@ import { import { changeActiveDatastore } from '../../../datastores'; import { setActiveInstitution } from '../../../institution'; import { setA11yMessage } from '../../../a11y'; -import { affiliationCookieSetter, setActiveAffilitation } from '../../../affiliation'; +import { setActiveAffilitation } from '../../../affiliation'; import PropTypes from 'prop-types'; function handleURLState ({ @@ -42,7 +42,6 @@ function handleURLState ({ const urlState = getStateFromURL({ location }); let shouldRunSearch = false; props.setActiveAffilitation(urlState.affiliation); - affiliationCookieSetter(urlState.affiliation); if (datastoreUid) { // URL has state diff --git a/src/modules/reusable/components/Dialog/index.js b/src/modules/reusable/components/Dialog/index.js deleted file mode 100644 index 0325d59b1..000000000 --- a/src/modules/reusable/components/Dialog/index.js +++ /dev/null @@ -1,91 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import React, { useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; - -export default function Dialog ({ closeOnOutsideClick, onRequestClose, open, ...props }) { - const dialogRef = useRef(null); - const lastActiveElement = useRef(null); - - useEffect(() => { - const dialogNode = dialogRef.current; - if (!dialogNode.hasAttribute('open') && open) { - lastActiveElement.current = document.activeElement; - dialogNode.showModal(); - document.querySelector('body').style.overflow = 'hidden'; - } else { - dialogNode.close(); - lastActiveElement?.current?.focus(); - } - }, [open]); - - useEffect(() => { - const dialogNode = dialogRef.current; - const handleCancel = (event) => { - event.preventDefault(); - onRequestClose(); - }; - dialogNode.addEventListener('cancel', handleCancel); - return () => { - dialogNode.removeEventListener('cancel', handleCancel); - document.querySelector('body').style.overflow = 'auto'; - }; - }, [onRequestClose]); - - function handleOutsideClick (event) { - const dialogNode = dialogRef.current; - if (closeOnOutsideClick && event.target === dialogNode) { - onRequestClose(); - } - } - - return ( - { - return handleOutsideClick(event); - }} - > -
-
- ); -} - -Dialog.propTypes = { - closeOnOutsideClick: PropTypes.bool, - onRequestClose: PropTypes.func, - open: PropTypes.bool -}; diff --git a/src/modules/reusable/index.js b/src/modules/reusable/index.js index a69b01c16..ad680b789 100644 --- a/src/modules/reusable/index.js +++ b/src/modules/reusable/index.js @@ -1,6 +1,5 @@ import Alert from './components/Alert'; import Button from './components/Button'; -import Dialog from './components/Dialog'; import Tag from './components/Tag'; import Breadcrumb from './components/Breadcrumb'; import Pagination from './components/Pagination'; @@ -14,7 +13,6 @@ import ResourceAccess from './components/ResourceAccess'; export { Alert, Button, - Dialog, Tag, Breadcrumb, Pagination, diff --git a/src/stylesheets/main.css b/src/stylesheets/main.css index 9e3224676..22b205c6f 100644 --- a/src/stylesheets/main.css +++ b/src/stylesheets/main.css @@ -2492,7 +2492,6 @@ input.multiselect-search { background: #FAFAFA; border-radius: 4px; padding: 0.75rem 1rem; - margin-bottom: 1rem; } .institution-select-container .dropdown { From a479f7cad2074e2e4f5f982ff5839d2be115c9fb Mon Sep 17 00:00:00 2001 From: "Erin E. Sullivan" Date: Fri, 29 Sep 2023 15:18:38 -0400 Subject: [PATCH 2/2] Adding `datastore` prop to `FlintAlerts`. --- src/modules/pages/components/DatastorePage/container.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/pages/components/DatastorePage/container.js b/src/modules/pages/components/DatastorePage/container.js index 3895826a4..fcca0c8c7 100644 --- a/src/modules/pages/components/DatastorePage/container.js +++ b/src/modules/pages/components/DatastorePage/container.js @@ -102,7 +102,7 @@ class DatastorePageContainer extends React.Component { <> - +