(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 { pathname } = useLocation()
const dispatch = useDispatch() const dispatch = useDispatch()
const { content, loader } = useSelector((state: ApplicationState) => state) const { content, header, loader } = useSelector((state: ApplicationState) => state)
const {
siteName = ""
} = content ? content : {}
useEffect(() => { useEffect(() => {
dispatch(settingsActionCreators.requestContent()) dispatch(settingsActionCreators.requestContent())
@ -60,14 +55,19 @@ const App = () => {
}) })
}, [pathname]) }, [pathname])
const {
title = "",
link = {},
meta = {}
} = header ? header : {}
return <> return <>
<Helmet> <Helmet>
<title>{siteName}</title> <title>{title}</title>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
{Object.keys(link).map((rel, index) => <link key={index} rel={rel} href={link[index]} />)}
<link rel="canonical" href="http://mysite.com/example" /> {Object.keys(meta).map((name, index) => <meta key={index} name={name} content={meta[index]} />)}
<meta name="description" content="react-redux" />
</Helmet> </Helmet>
<Routes> <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 { export interface RequestModel {
[key: string]: string | undefined [key: string]: string | undefined
} }
export interface ResponseModel { } export interface ResponseModel {}
export interface AddressPageSectionModel extends PageSectionModel { export interface AddressPageSectionModel extends PageSectionModel {
firstName: FormItemModel, firstName: FormItemModel,
@ -19,7 +19,7 @@ export interface AddressPageSectionModel extends PageSectionModel {
} }
export interface PageModel { export interface PageModel {
header: HeaderModel,
} }
export interface PageSectionModel { 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 { ResponseModel } from "./abstractions"
import * as Pages from "./pages" import * as Pages from "./pages"
@ -32,8 +32,9 @@ export interface GetShopCartResponseModel extends ResponseModel {
// Static content response model // Static content response model
export interface GetContentResponseModel extends ResponseModel { export interface GetContentResponseModel extends ResponseModel {
siteName: string, siteName: string,
siteUrl: string,
helmet: any, header: HeaderModel,
localization: LocalizationModel, localization: LocalizationModel,

View File

@ -7,13 +7,14 @@ import { useDispatch, useSelector } from 'react-redux'
import { ApplicationState } from '../../store' import { ApplicationState } from '../../store'
import { actionCreators as loaderActionCreators } from '../../store/reducers/Loader' import { actionCreators as loaderActionCreators } from '../../store/reducers/Loader'
import { actionCreators as blogFeaturedActionCreators } from '../../store/reducers/BlogFeatured' import { actionCreators as blogFeaturedActionCreators } from '../../store/reducers/BlogFeatured'
import { actionCreators as headerActionCreators } from '../../store/reducers/Header'
// Reactstrap // Reactstrap
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap' import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
// Models (interfaces) // Models (interfaces)
import { CallToActionSectionModel, FeaturedBlogsSectionModel, FeaturesSectionModel, TestimonialsSectionModel, TitleSectionModel } from '../../models/pageSections' import { CallToActionSectionModel, FeaturedBlogsSectionModel, FeaturesSectionModel, TestimonialsSectionModel, TitleSectionModel } from '../../models/pageSections'
import { BlogItemModel, FeatureModel, TestimonialModel } from '../../models' import { BlogItemModel, FeatureModel, HeaderModel, TestimonialModel } from '../../models'
// Custom components // Custom components
import { FeatherIcon } from '../../components/FeatherIcons' import { FeatherIcon } from '../../components/FeatherIcons'
@ -155,9 +156,9 @@ const FeaturedBlogsSection: FC<FeaturedBlogs> = ({
</section> </section>
const CallToActionSection: FC<CallToActionSectionModel> = ({ const CallToActionSection: FC<CallToActionSectionModel> = ({
title = "", title,
text = "", text,
privacyDisclaimer = "", privacyDisclaimer,
email = { email = {
placeHolder: "", placeHolder: "",
title: "" title: ""
@ -202,12 +203,37 @@ const Home = () => {
}, 1000) }, 1000)
}, [content?.isLoading, blogFeatured?.isLoading]) }, [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 <> return <>
<TitleSection {...page?.titleSection} /> <TitleSection {...titleSection} />
<FeaturesSection {...page?.featuresSection} /> <FeaturesSection {...featuresSection} />
<TestimonialsSection {...page?.testimonialsSection} /> <TestimonialsSection {...testimonialsSection} />
<FeaturedBlogsSection items={blogFeatured?.items} {...page?.featuredBlogsSection} /> <FeaturedBlogsSection items={blogFeatured?.items} {...featuredBlogsSection} />
<CallToActionSection {...page?.callToActionSection} /> <CallToActionSection {...callToActionSection} />
</> </>
} }

View File

@ -4,6 +4,7 @@ import * as BlogFeatured from './reducers/BlogFeatured'
import * as BlogItem from './reducers/BlogItem' import * as BlogItem from './reducers/BlogItem'
import * as Counter from './reducers/Counter' import * as Counter from './reducers/Counter'
import * as Header from './reducers/Header'
import * as Loader from './reducers/Loader' import * as Loader from './reducers/Loader'
import * as Content from './reducers/Content' import * as Content from './reducers/Content'
@ -27,6 +28,7 @@ export interface ApplicationState {
content: Content.ContentState | undefined content: Content.ContentState | undefined
counter: Counter.CounterState | undefined counter: Counter.CounterState | undefined
header: Header.HeaderState | undefined
loader: Loader.LoaderState | undefined loader: Loader.LoaderState | undefined
shopCatalog: ShopCatalog.ShopCatalogState | undefined shopCatalog: ShopCatalog.ShopCatalogState | undefined
@ -51,6 +53,7 @@ export const reducers = {
content: Content.reducer, content: Content.reducer,
counter: Counter.reducer, counter: Counter.reducer,
header: Header.reducer,
loader: Loader.reducer, loader: Loader.reducer,
shopCatalog: ShopCatalog.reducer, shopCatalog: ShopCatalog.reducer,

View File

@ -1,5 +1,6 @@
import { Action, Reducer } from 'redux' import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../' import { AppThunkAction } from '../'
import { ReservedWords } from '../../enumerations'
import { GetContentRequestModel } from '../../models/requests' import { GetContentRequestModel } from '../../models/requests'
import { GetContentResponseModel } from '../../models/responses' import { GetContentResponseModel } from '../../models/responses'
@ -36,17 +37,15 @@ export const actionCreators = {
const unloadedState: ContentState = { const unloadedState: ContentState = {
siteName: "MAKS-IT", siteName: "MAKS-IT",
siteUrl: "https://maks-it.com",
helmet: { header: {
title: "{siteName}", title: `${ReservedWords.siteName}`,
meta: { meta: {
chartset: "utf-8", chartset: "utf-8",
description: "react-redux", "google-site-verification": ""
"google-site-verification": "",
robots: "noindex, nofollow"
}, },
link: { link: {
canonical: ""
} }
}, },
@ -94,11 +93,20 @@ const unloadedState: ContentState = {
{ target: "/signin", title: "Sing in" }, { target: "/signin", title: "Sing in" },
{ target: "/signup", title: "Sign up" }, { 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: [], sideMenu: [],
homePage: { homePage: {
header: {
title: `Home - ${ReservedWords.siteName}`,
meta: {
description: "Single-page application home page",
},
link: {
canonical: `${ReservedWords.siteUrl}`
}
},
titleSection: { titleSection: {
title: "Hello, World! by Redux", title: "Hello, World! by Redux",
text: `<p>Welcome to your new single-page application, built with:</p> text: `<p>Welcome to your new single-page application, built with:</p>
@ -156,6 +164,15 @@ const unloadedState: ContentState = {
}, },
shopCatalog: { shopCatalog: {
header: {
title: `Shop catalog - ${ReservedWords.siteName}`,
meta: {
description: "Single-page application shop catalog",
},
link: {
canonical: ""
}
},
titleSection: { titleSection: {
title: "Shop in style", title: "Shop in style",
text: "With this shop hompeage template" text: "With this shop hompeage template"
@ -166,6 +183,15 @@ const unloadedState: ContentState = {
}, },
shopItem: { shopItem: {
header: {
title: `${ReservedWords.productTitle} - ${ReservedWords.siteName}`,
meta: {
description: "Single-page application shop item",
},
link: {
canonical: ""
}
},
productSection: { productSection: {
availableQuantity: "Available Qty.", availableQuantity: "Available Qty.",
addToCart: "Add to cart" addToCart: "Add to cart"
@ -177,13 +203,22 @@ const unloadedState: ContentState = {
}, },
shopCart: { shopCart: {
header: {
title: `Shop cart - ${ReservedWords.siteName}`,
meta: {
description: "Single-page application shop cart",
},
link: {
canonical: ""
}
},
titleSection: { titleSection: {
title: "Shopping Cart", title: "Shopping Cart",
text: "items in your cart" text: "items in your cart"
}, },
productsSection: { productsSection: {
title: "Shopping Cart", title: "Shopping Cart",
text: "{quantity} items in your cart", text: `${ReservedWords.quantity} items in your cart`,
product: "Product", product: "Product",
price: "Price", price: "Price",
quantity: "Quantity", quantity: "Quantity",
@ -199,6 +234,15 @@ const unloadedState: ContentState = {
}, },
shopCheckout: { shopCheckout: {
header: {
title: `Shop - checkout ${ReservedWords.siteName}`,
meta: {
description: "Single-page application checkout",
},
link: {
canonical: ""
}
},
titleSection: { titleSection: {
title: "Checkout", 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." 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: { summarySection: {
title: "Your cart", title: "Your cart",
total: "Total ({currency})", total: `Total (${ReservedWords.currency})`,
promoCode: { promoCode: {
placeHolder: "Promo code" placeHolder: "Promo code"
}, },
@ -315,18 +359,36 @@ const unloadedState: ContentState = {
}, },
blogCatalog: { blogCatalog: {
header: {
title: `Blog catalog - ${ReservedWords.siteName}`,
meta: {
description: "Single-page application blog catalog",
},
link: {
canonical: ""
}
},
titleSection: { titleSection: {
title: "Welcome to Blog Home!", title: "Welcome to Blog Home!",
text: "A Bootstrap 5 starter layout for your next blog homepage" text: "A Bootstrap 5 starter layout for your next blog homepage"
}, },
featuredBlogSection: { featuredBlogSection: {
readTime: "{date} Time to read: {readTime} min" readTime: `${ReservedWords.date} Time to read: ${ReservedWords.readTime} min`
}, },
}, },
blogItem: { blogItem: {
header: {
title: `${ReservedWords.blogTitle} - ${ReservedWords.siteName}`,
meta: {
description: "Single-page application blog item",
},
link: {
canonical: ""
}
},
titleSection: { titleSection: {
postedOnBy: "Posted on {date} by {nickName}" postedOnBy: `Posted on ${ReservedWords.date} by ${ReservedWords.nickName}`
}, },
commentsSection: { commentsSection: {
leaveComment: "Join the discussion and leave a comment!" leaveComment: "Join the discussion and leave a comment!"
@ -335,6 +397,16 @@ const unloadedState: ContentState = {
}, },
signIn: { signIn: {
header: {
title: `Sign in - ${ReservedWords.siteName}`,
meta: {
description: "Single-page application sign in",
robots: "noindex, nofollow"
},
link: {
canonical: ""
}
},
title: "Sign in", title: "Sign in",
email: { email: {
title: "Email address", title: "Email address",
@ -355,6 +427,16 @@ const unloadedState: ContentState = {
}, },
signUp: { signUp: {
header: {
title: "Sign up - {siteName}",
meta: {
description: "Single-page application sign up",
robots: "noindex, nofollow"
},
link: {
canonical: ""
}
},
title: "Sign up", title: "Sign up",
username: { username: {
title: "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
}
}