(feat): header model and reducer, reserved words consolidation

This commit is contained in:
Maksym Sadovnychyy 2022-08-01 19:14:25 +02:00
parent 901b7f02e3
commit 8fe4d93000
9 changed files with 267 additions and 36 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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