(feat): header model and reducer, reserved words consolidation
This commit is contained in:
parent
901b7f02e3
commit
8fe4d93000
@ -42,12 +42,7 @@ const App = () => {
|
||||
const { pathname } = useLocation()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const { content, loader } = useSelector((state: ApplicationState) => state)
|
||||
|
||||
|
||||
const {
|
||||
siteName = ""
|
||||
} = content ? content : {}
|
||||
const { content, header, loader } = useSelector((state: ApplicationState) => state)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(settingsActionCreators.requestContent())
|
||||
@ -60,14 +55,19 @@ const App = () => {
|
||||
})
|
||||
}, [pathname])
|
||||
|
||||
|
||||
const {
|
||||
title = "",
|
||||
link = {},
|
||||
meta = {}
|
||||
} = header ? header : {}
|
||||
|
||||
return <>
|
||||
<Helmet>
|
||||
<title>{siteName}</title>
|
||||
<title>{title}</title>
|
||||
<meta charSet="utf-8" />
|
||||
|
||||
<link rel="canonical" href="http://mysite.com/example" />
|
||||
|
||||
<meta name="description" content="react-redux" />
|
||||
{Object.keys(link).map((rel, index) => <link key={index} rel={rel} href={link[index]} />)}
|
||||
{Object.keys(meta).map((name, index) => <meta key={index} name={name} content={meta[index]} />)}
|
||||
</Helmet>
|
||||
|
||||
<Routes>
|
||||
|
||||
17
webapi/ClientApp/src/enumerations/index.ts
Normal file
17
webapi/ClientApp/src/enumerations/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
|
||||
enum ReservedWords {
|
||||
siteName = "{siteName}",
|
||||
siteUrl = "{siteUrl}",
|
||||
quantity = "{quantity}",
|
||||
productTitle = "{productTitle}",
|
||||
currency = "{currency}",
|
||||
date = "{date}",
|
||||
readTime = "{readTime}",
|
||||
blogTitle = "{blogTitle}",
|
||||
nickName = "{nickName}"
|
||||
}
|
||||
|
||||
export {
|
||||
ReservedWords
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
import { AuthorModel, FormItemModel, ImageModel } from "./"
|
||||
import { AuthorModel, FormItemModel, HeaderModel, ImageModel } from "./"
|
||||
|
||||
|
||||
export interface RequestModel {
|
||||
[key: string]: string | undefined
|
||||
}
|
||||
|
||||
export interface ResponseModel { }
|
||||
export interface ResponseModel {}
|
||||
|
||||
export interface AddressPageSectionModel extends PageSectionModel {
|
||||
firstName: FormItemModel,
|
||||
@ -19,7 +19,7 @@ export interface AddressPageSectionModel extends PageSectionModel {
|
||||
}
|
||||
|
||||
export interface PageModel {
|
||||
|
||||
header: HeaderModel,
|
||||
}
|
||||
|
||||
export interface PageSectionModel {
|
||||
|
||||
@ -93,3 +93,17 @@ export interface TestimonialModel {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export interface HeaderLink {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
[key: string]: string
|
||||
}
|
||||
export interface HeaderModel {
|
||||
title: string,
|
||||
link: HeaderLink,
|
||||
meta: Meta
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { BlogItemModel, CategoryModel, CommentModel, LocalizationModel, MenuItemModel, PaginationModel, RouteModel, ShopItemModel } from "./"
|
||||
import { BlogItemModel, CategoryModel, CommentModel, HeaderModel, LocalizationModel, MenuItemModel, PaginationModel, RouteModel, ShopItemModel } from "./"
|
||||
import { ResponseModel } from "./abstractions"
|
||||
import * as Pages from "./pages"
|
||||
|
||||
@ -32,8 +32,9 @@ export interface GetShopCartResponseModel extends ResponseModel {
|
||||
// Static content response model
|
||||
export interface GetContentResponseModel extends ResponseModel {
|
||||
siteName: string,
|
||||
siteUrl: string,
|
||||
|
||||
helmet: any,
|
||||
header: HeaderModel,
|
||||
|
||||
localization: LocalizationModel,
|
||||
|
||||
|
||||
@ -7,13 +7,14 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../store'
|
||||
import { actionCreators as loaderActionCreators } from '../../store/reducers/Loader'
|
||||
import { actionCreators as blogFeaturedActionCreators } from '../../store/reducers/BlogFeatured'
|
||||
import { actionCreators as headerActionCreators } from '../../store/reducers/Header'
|
||||
|
||||
// Reactstrap
|
||||
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
|
||||
|
||||
// Models (interfaces)
|
||||
import { CallToActionSectionModel, FeaturedBlogsSectionModel, FeaturesSectionModel, TestimonialsSectionModel, TitleSectionModel } from '../../models/pageSections'
|
||||
import { BlogItemModel, FeatureModel, TestimonialModel } from '../../models'
|
||||
import { BlogItemModel, FeatureModel, HeaderModel, TestimonialModel } from '../../models'
|
||||
|
||||
// Custom components
|
||||
import { FeatherIcon } from '../../components/FeatherIcons'
|
||||
@ -155,9 +156,9 @@ const FeaturedBlogsSection: FC<FeaturedBlogs> = ({
|
||||
</section>
|
||||
|
||||
const CallToActionSection: FC<CallToActionSectionModel> = ({
|
||||
title = "",
|
||||
text = "",
|
||||
privacyDisclaimer = "",
|
||||
title,
|
||||
text,
|
||||
privacyDisclaimer,
|
||||
email = {
|
||||
placeHolder: "",
|
||||
title: ""
|
||||
@ -202,12 +203,37 @@ const Home = () => {
|
||||
}, 1000)
|
||||
}, [content?.isLoading, blogFeatured?.isLoading])
|
||||
|
||||
const {
|
||||
header = {},
|
||||
titleSection = {
|
||||
title: "",
|
||||
text: ""
|
||||
},
|
||||
featuresSection = {},
|
||||
testimonialsSection = {},
|
||||
featuredBlogsSection = {},
|
||||
callToActionSection = {
|
||||
title: "",
|
||||
text: "",
|
||||
privacyDisclaimer: "",
|
||||
email: {
|
||||
placeHolder: "",
|
||||
title: ""
|
||||
}
|
||||
}
|
||||
} = content?.homePage ? content.homePage : {}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(headerActionCreators.updateHeader(header as HeaderModel))
|
||||
}, [header])
|
||||
|
||||
|
||||
return <>
|
||||
<TitleSection {...page?.titleSection} />
|
||||
<FeaturesSection {...page?.featuresSection} />
|
||||
<TestimonialsSection {...page?.testimonialsSection} />
|
||||
<FeaturedBlogsSection items={blogFeatured?.items} {...page?.featuredBlogsSection} />
|
||||
<CallToActionSection {...page?.callToActionSection} />
|
||||
<TitleSection {...titleSection} />
|
||||
<FeaturesSection {...featuresSection} />
|
||||
<TestimonialsSection {...testimonialsSection} />
|
||||
<FeaturedBlogsSection items={blogFeatured?.items} {...featuredBlogsSection} />
|
||||
<CallToActionSection {...callToActionSection} />
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import * as BlogFeatured from './reducers/BlogFeatured'
|
||||
import * as BlogItem from './reducers/BlogItem'
|
||||
|
||||
import * as Counter from './reducers/Counter'
|
||||
import * as Header from './reducers/Header'
|
||||
import * as Loader from './reducers/Loader'
|
||||
|
||||
import * as Content from './reducers/Content'
|
||||
@ -27,6 +28,7 @@ export interface ApplicationState {
|
||||
content: Content.ContentState | undefined
|
||||
|
||||
counter: Counter.CounterState | undefined
|
||||
header: Header.HeaderState | undefined
|
||||
loader: Loader.LoaderState | undefined
|
||||
|
||||
shopCatalog: ShopCatalog.ShopCatalogState | undefined
|
||||
@ -51,6 +53,7 @@ export const reducers = {
|
||||
content: Content.reducer,
|
||||
|
||||
counter: Counter.reducer,
|
||||
header: Header.reducer,
|
||||
loader: Loader.reducer,
|
||||
|
||||
shopCatalog: ShopCatalog.reducer,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Action, Reducer } from 'redux'
|
||||
import { AppThunkAction } from '../'
|
||||
import { ReservedWords } from '../../enumerations'
|
||||
|
||||
import { GetContentRequestModel } from '../../models/requests'
|
||||
import { GetContentResponseModel } from '../../models/responses'
|
||||
@ -36,17 +37,15 @@ export const actionCreators = {
|
||||
|
||||
const unloadedState: ContentState = {
|
||||
siteName: "MAKS-IT",
|
||||
siteUrl: "https://maks-it.com",
|
||||
|
||||
helmet: {
|
||||
title: "{siteName}",
|
||||
header: {
|
||||
title: `${ReservedWords.siteName}`,
|
||||
meta: {
|
||||
chartset: "utf-8",
|
||||
description: "react-redux",
|
||||
"google-site-verification": "",
|
||||
robots: "noindex, nofollow"
|
||||
"google-site-verification": ""
|
||||
},
|
||||
link: {
|
||||
canonical: ""
|
||||
}
|
||||
},
|
||||
|
||||
@ -94,11 +93,20 @@ const unloadedState: ContentState = {
|
||||
{ target: "/signin", title: "Sing in" },
|
||||
{ target: "/signup", title: "Sign up" },
|
||||
|
||||
{ target: "/shop/cart", icon: "shopping-cart", title: "Cart ({quantity})" }
|
||||
{ target: "/shop/cart", icon: "shopping-cart", title: `Cart (${ReservedWords.quantity})` }
|
||||
],
|
||||
sideMenu: [],
|
||||
|
||||
homePage: {
|
||||
header: {
|
||||
title: `Home - ${ReservedWords.siteName}`,
|
||||
meta: {
|
||||
description: "Single-page application home page",
|
||||
},
|
||||
link: {
|
||||
canonical: `${ReservedWords.siteUrl}`
|
||||
}
|
||||
},
|
||||
titleSection: {
|
||||
title: "Hello, World! by Redux",
|
||||
text: `<p>Welcome to your new single-page application, built with:</p>
|
||||
@ -156,6 +164,15 @@ const unloadedState: ContentState = {
|
||||
},
|
||||
|
||||
shopCatalog: {
|
||||
header: {
|
||||
title: `Shop catalog - ${ReservedWords.siteName}`,
|
||||
meta: {
|
||||
description: "Single-page application shop catalog",
|
||||
},
|
||||
link: {
|
||||
canonical: ""
|
||||
}
|
||||
},
|
||||
titleSection: {
|
||||
title: "Shop in style",
|
||||
text: "With this shop hompeage template"
|
||||
@ -166,6 +183,15 @@ const unloadedState: ContentState = {
|
||||
},
|
||||
|
||||
shopItem: {
|
||||
header: {
|
||||
title: `${ReservedWords.productTitle} - ${ReservedWords.siteName}`,
|
||||
meta: {
|
||||
description: "Single-page application shop item",
|
||||
},
|
||||
link: {
|
||||
canonical: ""
|
||||
}
|
||||
},
|
||||
productSection: {
|
||||
availableQuantity: "Available Qty.",
|
||||
addToCart: "Add to cart"
|
||||
@ -177,13 +203,22 @@ const unloadedState: ContentState = {
|
||||
},
|
||||
|
||||
shopCart: {
|
||||
header: {
|
||||
title: `Shop cart - ${ReservedWords.siteName}`,
|
||||
meta: {
|
||||
description: "Single-page application shop cart",
|
||||
},
|
||||
link: {
|
||||
canonical: ""
|
||||
}
|
||||
},
|
||||
titleSection: {
|
||||
title: "Shopping Cart",
|
||||
text: "items in your cart"
|
||||
},
|
||||
productsSection: {
|
||||
title: "Shopping Cart",
|
||||
text: "{quantity} items in your cart",
|
||||
text: `${ReservedWords.quantity} items in your cart`,
|
||||
product: "Product",
|
||||
price: "Price",
|
||||
quantity: "Quantity",
|
||||
@ -199,6 +234,15 @@ const unloadedState: ContentState = {
|
||||
},
|
||||
|
||||
shopCheckout: {
|
||||
header: {
|
||||
title: `Shop - checkout ${ReservedWords.siteName}`,
|
||||
meta: {
|
||||
description: "Single-page application checkout",
|
||||
},
|
||||
link: {
|
||||
canonical: ""
|
||||
}
|
||||
},
|
||||
titleSection: {
|
||||
title: "Checkout",
|
||||
text: "Below is an example form built entirely with Bootstrap’s form controls. Each required form group has a validation state that can be triggered by attempting to submit the form without completing it."
|
||||
@ -281,7 +325,7 @@ const unloadedState: ContentState = {
|
||||
},
|
||||
summarySection: {
|
||||
title: "Your cart",
|
||||
total: "Total ({currency})",
|
||||
total: `Total (${ReservedWords.currency})`,
|
||||
promoCode: {
|
||||
placeHolder: "Promo code"
|
||||
},
|
||||
@ -315,18 +359,36 @@ const unloadedState: ContentState = {
|
||||
},
|
||||
|
||||
blogCatalog: {
|
||||
header: {
|
||||
title: `Blog catalog - ${ReservedWords.siteName}`,
|
||||
meta: {
|
||||
description: "Single-page application blog catalog",
|
||||
},
|
||||
link: {
|
||||
canonical: ""
|
||||
}
|
||||
},
|
||||
titleSection: {
|
||||
title: "Welcome to Blog Home!",
|
||||
text: "A Bootstrap 5 starter layout for your next blog homepage"
|
||||
},
|
||||
featuredBlogSection: {
|
||||
readTime: "{date} Time to read: {readTime} min"
|
||||
readTime: `${ReservedWords.date} Time to read: ${ReservedWords.readTime} min`
|
||||
},
|
||||
},
|
||||
|
||||
blogItem: {
|
||||
header: {
|
||||
title: `${ReservedWords.blogTitle} - ${ReservedWords.siteName}`,
|
||||
meta: {
|
||||
description: "Single-page application blog item",
|
||||
},
|
||||
link: {
|
||||
canonical: ""
|
||||
}
|
||||
},
|
||||
titleSection: {
|
||||
postedOnBy: "Posted on {date} by {nickName}"
|
||||
postedOnBy: `Posted on ${ReservedWords.date} by ${ReservedWords.nickName}`
|
||||
},
|
||||
commentsSection: {
|
||||
leaveComment: "Join the discussion and leave a comment!"
|
||||
@ -335,6 +397,16 @@ const unloadedState: ContentState = {
|
||||
},
|
||||
|
||||
signIn: {
|
||||
header: {
|
||||
title: `Sign in - ${ReservedWords.siteName}`,
|
||||
meta: {
|
||||
description: "Single-page application sign in",
|
||||
robots: "noindex, nofollow"
|
||||
},
|
||||
link: {
|
||||
canonical: ""
|
||||
}
|
||||
},
|
||||
title: "Sign in",
|
||||
email: {
|
||||
title: "Email address",
|
||||
@ -355,6 +427,16 @@ const unloadedState: ContentState = {
|
||||
},
|
||||
|
||||
signUp: {
|
||||
header: {
|
||||
title: "Sign up - {siteName}",
|
||||
meta: {
|
||||
description: "Single-page application sign up",
|
||||
robots: "noindex, nofollow"
|
||||
},
|
||||
link: {
|
||||
canonical: ""
|
||||
}
|
||||
},
|
||||
title: "Sign up",
|
||||
username: {
|
||||
title: "Username",
|
||||
|
||||
88
webapi/ClientApp/src/store/reducers/Header.ts
Normal file
88
webapi/ClientApp/src/store/reducers/Header.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { Action, Reducer } from 'redux'
|
||||
import { AppThunkAction } from '..'
|
||||
import { ReservedWords } from '../../enumerations'
|
||||
|
||||
import { HeaderLink, HeaderModel } from '../../models'
|
||||
|
||||
// -----------------
|
||||
// STATE - This defines the type of data maintained in the Redux store.
|
||||
|
||||
export interface HeaderState extends HeaderModel {}
|
||||
|
||||
// -----------------
|
||||
// ACTIONS - These are serializable (hence replayable) descriptions of state transitions.
|
||||
// They do not themselves have any side-effects they just describe something that is going to happen.
|
||||
// Use @typeName and isActionType for type detection that works even after serialization/deserialization.
|
||||
|
||||
export interface ReceiveAction extends HeaderModel { type: 'RECEIVE_UPDATE_HEADER' }
|
||||
|
||||
|
||||
// Declare a 'discriminated union' type. This guarantees that all references to 'type' properties contain one of the
|
||||
// declared type strings (and not any other arbitrary string).
|
||||
export type KnownAction = ReceiveAction
|
||||
|
||||
// ----------------
|
||||
// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
|
||||
// They don't directly mutate state, but they can have external side-effects (such as loading data).
|
||||
|
||||
export const actionCreators = {
|
||||
|
||||
updateHeader: (props: HeaderModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
|
||||
|
||||
const siteName = getState().content?.siteName
|
||||
const baseHeader = getState().content?.header
|
||||
|
||||
const title = (props?.title
|
||||
? props.title
|
||||
: baseHeader?.title
|
||||
? baseHeader.title
|
||||
: ReservedWords.siteName).replace(ReservedWords.siteName, siteName ? siteName : "")
|
||||
|
||||
// assign default link
|
||||
const link = baseHeader?.link
|
||||
? baseHeader.link
|
||||
: {}
|
||||
|
||||
// assign default meta
|
||||
const meta = baseHeader?.meta
|
||||
? baseHeader.meta
|
||||
: {}
|
||||
|
||||
// overrid link
|
||||
if (props.link)
|
||||
Object.keys(props.link).forEach(key => link[key] = props.link[key])
|
||||
|
||||
// override meta
|
||||
if(props.meta)
|
||||
Object.keys(props.meta).forEach(key => meta[key] = props.meta[key])
|
||||
|
||||
dispatch({ type: 'RECEIVE_UPDATE_HEADER', ...{ title, link, meta } })
|
||||
}
|
||||
}
|
||||
|
||||
const unloadedState: HeaderState = {
|
||||
title: "",
|
||||
link: {},
|
||||
meta: {}
|
||||
}
|
||||
|
||||
|
||||
// ----------------
|
||||
// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
|
||||
|
||||
export const reducer: Reducer<HeaderState> = (state: HeaderState | undefined, incomingAction: Action): HeaderState => {
|
||||
if (state === undefined) {
|
||||
return unloadedState
|
||||
}
|
||||
|
||||
const action = incomingAction as KnownAction
|
||||
switch (action.type) {
|
||||
case 'RECEIVE_UPDATE_HEADER':
|
||||
return {
|
||||
...action
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user