(feat): BlogItem reducer

This commit is contained in:
Maksym Sadovnychyy 2022-06-03 23:01:39 +02:00
parent 268c1d0060
commit e1ae3f20e5
14 changed files with 316 additions and 78 deletions

View File

@ -0,0 +1,46 @@
import React, { FC } from 'react'
import { Card, CardBody } from 'reactstrap'
import { CommentsSectionModel } from '../../models/pageSections'
const Comments: FC<CommentsSectionModel> = ({
comments = []
}) => {
return <section className="mb-5">
<Card className="card bg-light">
<CardBody className="card-body">
<form className="mb-4">
<textarea className="form-control" rows={3} placeholder="Join the discussion and leave a comment!"></textarea>
</form>
{comments.map((comment, index) => <div key={index} className={`d-flex ${index < comments.length - 1 ? 'mb-4' : ''}`}>
<div className="flex-shrink-0">
<img className="rounded-circle" {...comment.author.image} />
</div>
<div className="ms-3">
<div className="fw-bold">{comment.author.nickName}</div>
{comment.comment}
{comment.responses? comment.responses.map((response, index) => <div key={index} className="d-flex mt-4">
<div className="flex-shrink-0">
<img className="rounded-circle" {...response.author.image} />
</div>
<div className="ms-3">
<div className="fw-bold">{response.author.nickName}</div>
{response.comment}
</div>
</div>) : ''}
</div>
</div>)}
</CardBody>
</Card>
</section>
}
export {
Comments
}

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { Card, CardBody, CardHeader, Col, Row } from 'reactstrap' import { Card, CardBody, CardHeader, Col, Row } from 'reactstrap'
import { CategoryModel } from '../../../models' import { CategoryModel } from '../../models'
const Search = () => { const Search = () => {
return <Card className="mb-4"> return <Card className="mb-4">

View File

@ -2,7 +2,7 @@ import { AuthorModel, ImageModel } from "./"
export interface RequestModel { export interface RequestModel {
[key: string]: string | undefined
} }
export interface ResponseModel { export interface ResponseModel {

View File

@ -67,3 +67,8 @@ export interface FormItemModel {
placeHolder?: string placeHolder?: string
} }
export interface CommentModel {
author: AuthorModel,
comment: string,
responses?: CommentModel []
}

View File

@ -1,4 +1,4 @@
import { BlogItemModel, FeatureModel, FormItemModel, ImageModel, MenuItemModel, TestimonialsModel } from "./" import { BlogItemModel, CommentModel, FeatureModel, FormItemModel, ImageModel, MenuItemModel, TestimonialsModel } from "./"
import { PageSectionModel } from "./abstractions" import { PageSectionModel } from "./abstractions"
export interface CallToActionSectionModel extends PageSectionModel { export interface CallToActionSectionModel extends PageSectionModel {
@ -23,3 +23,7 @@ export interface TitleSectionModel extends PageSectionModel {
primaryLink?: MenuItemModel, primaryLink?: MenuItemModel,
secondaryLink?: MenuItemModel secondaryLink?: MenuItemModel
} }
export interface CommentsSectionModel extends PageSectionModel {
comments?: CommentModel []
}

View File

@ -16,3 +16,7 @@ export interface ShopCatalogPageModel extends PageModel {
export interface BlogCatalogPageModel extends PageModel { export interface BlogCatalogPageModel extends PageModel {
titleSection: TitleSectionModel titleSection: TitleSectionModel
} }
export interface BlogPageModel extends PageModel {
}

View File

@ -1,22 +1,24 @@
import { RequestModel } from "./abstractions"
export interface GetShopCatalogRequestModel { export interface GetShopCatalogRequestModel extends RequestModel {
[key: string]: string | undefined
category?: string, category?: string,
searchText?: string, searchText?: string,
currentPage?: string, currentPage?: string,
itemsPerPage?: string itemsPerPage?: string
} }
export interface GetBlogCatalogRequestModel { export interface GetBlogCatalogRequestModel extends RequestModel {
[key: string]: string | undefined
category?: string, category?: string,
searchText?: string, searchText?: string,
currentPage?: string, currentPage?: string,
itemsPerPage?: string itemsPerPage?: string
} }
export interface GetStaticContentRequestModel { export interface GetBlogItemRequestModel extends RequestModel {
[key: string]: string | undefined slug: string
}
export interface GetStaticContentRequestModel extends RequestModel {
locale?: string locale?: string
} }

View File

@ -1,4 +1,4 @@
import { BlogItemModel, CategoryModel, MenuItemModel, PaginationModel, RouteModel, ShopItemModel } from "./" import { BlogItemModel, CategoryModel, CommentModel, MenuItemModel, PaginationModel, RouteModel, ShopItemModel } from "./"
import { ResponseModel } from "./abstractions" import { ResponseModel } from "./abstractions"
import { BlogCatalogPageModel, HomePageModel, ShopCatalogPageModel } from "./pages" import { BlogCatalogPageModel, HomePageModel, ShopCatalogPageModel } from "./pages"
@ -8,6 +8,10 @@ export interface GetBlogCatalogResponseModel extends ResponseModel {
blogItemsPagination: PaginationModel<BlogItemModel> blogItemsPagination: PaginationModel<BlogItemModel>
} }
export interface GetBlogItemResponseModel extends ResponseModel {
comments: CommentModel []
}
export interface GetShopCatalogResponseModel extends ResponseModel { export interface GetShopCatalogResponseModel extends ResponseModel {
shopItemsPagination: PaginationModel<ShopItemModel> shopItemsPagination: PaginationModel<ShopItemModel>
} }

View File

@ -11,7 +11,7 @@ import { dateFormat, findRoutes } from '../../../functions'
import { BlogItemModel, PaginationModel } from '../../../models' import { BlogItemModel, PaginationModel } from '../../../models'
import { ApplicationState } from '../../../store' import { ApplicationState } from '../../../store'
import { Categories, Empty, Search } from '../SideWidgets' import { Categories, Empty, Search } from '../../../components/SideWidgets'
import { TitleSectionModel } from '../../../models/pageSections' import { TitleSectionModel } from '../../../models/pageSections'
import { Pagination } from '../../../components/Pagination' import { Pagination } from '../../../components/Pagination'
@ -27,14 +27,20 @@ const TitleSection: FC<TitleSectionModel> = (props) => {
</header> </header>
} }
const FeaturedBlog: FC<BlogItemModel> = (props) => { interface FeaturedBlogModel extends BlogItemModel {
const { id, slug, badge, image, title, shortText, author, created, readTime, likes, tags } = props currentPage: number
path: string,
}
const FeaturedBlog: FC<FeaturedBlogModel> = (props) => {
const { id, slug, badge, image, title, shortText, author, created, readTime, likes, tags, currentPage, path } = props
return <Card className="mb-4 shadow border-0"> return <Card className="mb-4 shadow border-0">
<CardImg top {...image} /> <CardImg top {...image} />
<CardBody className="p-4"> <CardBody className="p-4">
<div className="badge bg-primary bg-gradient rounded-pill mb-2">{badge}</div> <div className="badge bg-primary bg-gradient rounded-pill mb-2">{badge}</div>
<Link className="text-decoration-none link-dark stretched-link" to={`/blog/${slug}`}> <Link className="text-decoration-none link-dark stretched-link" to={`${path}/${currentPage}/${slug}`}>
<h5 className="card-title mb-3">{title}</h5> <h5 className="card-title mb-3">{title}</h5>
</Link> </Link>
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: shortText }}></p> <p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: shortText }}></p>
@ -122,7 +128,7 @@ const BlogCatalog = () => {
<Container fluid> <Container fluid>
<Row> <Row>
<Col> <Col>
{blogCatalog?.featuredBlog ? <FeaturedBlog {...blogCatalog.featuredBlog} /> : ''} {blogCatalog?.featuredBlog ? <FeaturedBlog path={path} currentPage={blogCatalog.blogItemsPagination.currentPage} {...blogCatalog.featuredBlog} /> : ''}
<Row> <Row>
{blogCatalog?.blogItemsPagination ? <BlogItemsPagination path={path} {...blogCatalog.blogItemsPagination} /> : '' } {blogCatalog?.blogItemsPagination ? <BlogItemsPagination path={path} {...blogCatalog.blogItemsPagination} /> : '' }
</Row> </Row>

View File

@ -1,13 +1,101 @@
import React from 'react' // React
import { Card, CardBody, CardHeader, Col, Container, Row } from 'reactstrap' import React, { useEffect } from 'react'
import { Categories, Empty, Search } from '../SideWidgets' import { useParams } from 'react-router-dom'
// Redux
import { useDispatch, useSelector } from 'react-redux'
import { actionCreators as loaderActionCreators } from '../../../store/reducers/Loader'
import { actionCreators as blogItemActionCreators } from '../../../store/reducers/BlogItem'
import { Col, Container, Row } from 'reactstrap'
import { Comments } from '../../../components/Comments'
import { Categories, Empty, Search } from '../../../components/SideWidgets'
import { CommentModel } from '../../../models'
import { ApplicationState } from '../../../store'
const comments : CommentModel [] = [
{
author: {
id: "",
nickName: "Commenter Name 1",
image: {
src: "https://dummyimage.com/50x50/ced4da/6c757d.jpg",
alt: "..."
}
},
comment: "If you're going to lead a space frontier, it has to be government; it'll never be private enterprise. Because the space frontier is dangerous, and it's expensive, and it has unquantified risks.",
responses: [
{
author: {
id: "",
nickName: "Commenter Name 4",
image: {
src: "https://dummyimage.com/50x50/ced4da/6c757d.jpg",
alt: "..."
}
},
comment: "And under those conditions, you cannot establish a capital-market evaluation of that enterprise. You can't get investors."
},
{
author: {
id: "",
nickName: "Commenter Name 3",
image: {
src: "https://dummyimage.com/50x50/ced4da/6c757d.jpg",
alt: "..."
}
},
comment: "When you put money directly to a problem, it makes a good headline."
}
]
},
{
author: {
id: "",
nickName: "Commenter Name 2",
image: {
src: "https://dummyimage.com/50x50/ced4da/6c757d.jpg",
alt: "..."
}
},
comment: "When I look at the universe and all the ways the universe wants to kill us, I find it hard to reconcile that with statements of beneficence."
}
]
const badges : string [] = [
"Web Design",
"Freebies"
]
const BlogItem = () => { const BlogItem = () => {
const params = useParams()
const dispatch = useDispatch()
const blogItem = useSelector((state: ApplicationState) => state.blogItem)
useEffect(() => {
if(params?.slug)
dispatch(blogItemActionCreators.requestBlogItem({
slug: params.slug
}))
}, [])
useEffect(() => {
blogItem?.isLoading
? dispatch(loaderActionCreators.show())
: setTimeout(() => {
dispatch(loaderActionCreators.hide())
}, 1000)
}, [blogItem?.isLoading])
const postItem = {
comments: []
}
return <Container fluid mt="5"> return <Container fluid mt="5">
<Row> <Row>
@ -17,11 +105,13 @@ const BlogItem = () => {
<header className="mb-4"> <header className="mb-4">
<h1 className="fw-bolder mb-1">Welcome to Blog Post!</h1> <h1 className="fw-bolder mb-1">Welcome to Blog Post!</h1>
<div className="text-muted fst-italic mb-2">Posted on January 1, 2022 by Start Bootstrap</div> <div className="text-muted fst-italic mb-2">Posted on January 1, 2022 by Start Bootstrap</div>
<a className="badge bg-secondary text-decoration-none link-light" href="#!">Web Design</a>
<a className="badge bg-secondary text-decoration-none link-light" href="#!">Freebies</a> {badges.map((badge, index) => <a key={index} className="badge bg-secondary text-decoration-none link-light" href="#!">{badge}</a>)}
</header> </header>
<figure className="mb-4"><img className="img-fluid rounded" src="https://dummyimage.com/900x400/ced4da/6c757d.jpg" alt="..." /></figure> <figure className="mb-4">
<img className="img-fluid rounded" src="https://dummyimage.com/900x400/ced4da/6c757d.jpg" alt="..." />
</figure>
<section className="mb-5"> <section className="mb-5">
<p className="fs-5 mb-4">Science is an enterprise that should be cherished as an activity of the free human mind. Because it transforms who we are, how we live, and it gives us an understanding of our place in the universe.</p> <p className="fs-5 mb-4">Science is an enterprise that should be cherished as an activity of the free human mind. Because it transforms who we are, how we live, and it gives us an understanding of our place in the universe.</p>
@ -33,53 +123,7 @@ const BlogItem = () => {
</section> </section>
</article> </article>
<Comments comments={comments} />
<section className="mb-5">
<div className="card bg-light">
<div className="card-body">
<form className="mb-4">
<textarea className="form-control" rows={3} placeholder="Join the discussion and leave a comment!"></textarea>
</form>
<div className="d-flex mb-4">
<div className="flex-shrink-0"><img className="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div className="ms-3">
<div className="fw-bold">Commenter Name</div>
If you're going to lead a space frontier, it has to be government; it'll never be private enterprise. Because the space frontier is dangerous, and it's expensive, and it has unquantified risks.
<div className="d-flex mt-4">
<div className="flex-shrink-0"><img className="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div className="ms-3">
<div className="fw-bold">Commenter Name</div>
And under those conditions, you cannot establish a capital-market evaluation of that enterprise. You can't get investors.
</div>
</div>
<div className="d-flex mt-4">
<div className="flex-shrink-0"><img className="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div className="ms-3">
<div className="fw-bold">Commenter Name</div>
When you put money directly to a problem, it makes a good headline.
</div>
</div>
</div>
</div>
<div className="d-flex">
<div className="flex-shrink-0"><img className="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div className="ms-3">
<div className="fw-bold">Commenter Name</div>
When I look at the universe and all the ways the universe wants to kill us, I find it hard to reconcile that with statements of beneficence.
</div>
</div>
</div>
</div>
</section>
</Col> </Col>

View File

@ -1,8 +1,7 @@
import { RequestModel } from "./models/abstractions"
export interface IRequest {
[key: string]: string | undefined
}
interface IFetchResult { interface IFetchResult {
status: number, status: number,
@ -13,7 +12,7 @@ const Post = () => {
} }
const Get = async <T>(apiUrl: string, props?: IRequest): Promise<T | null> => { const Get = async <T>(apiUrl: string, props?: RequestModel): Promise<T | null> => {
const url = new URL(apiUrl) const url = new URL(apiUrl)
if(props) { if(props) {

View File

@ -4,9 +4,11 @@ import * as Counter from './reducers/Counter'
import * as Loader from './reducers/Loader' import * as Loader from './reducers/Loader'
import * as Content from './reducers/Content' import * as Content from './reducers/Content'
import * as BlogCatalog from './reducers/BlogCatalog'
import * as ShopCatalog from './reducers/ShopCatalog'
import * as BlogCatalog from './reducers/BlogCatalog'
import * as BlogItem from './reducers/BlogItem'
import * as ShopCatalog from './reducers/ShopCatalog'
// The top-level state object // The top-level state object
export interface ApplicationState { export interface ApplicationState {
@ -16,7 +18,10 @@ export interface ApplicationState {
loader: Loader.LoaderState | undefined loader: Loader.LoaderState | undefined
content: Content.ContentState | undefined content: Content.ContentState | undefined
blogCatalog: BlogCatalog.BlogCatalogState | undefined blogCatalog: BlogCatalog.BlogCatalogState | undefined
blogItem: BlogItem.BlogItemState | undefined
shopCatalog: ShopCatalog.ShopCatalogState | undefined shopCatalog: ShopCatalog.ShopCatalogState | undefined
} }
@ -30,7 +35,10 @@ export const reducers = {
loader: Loader.reducer, loader: Loader.reducer,
content: Content.reducer, content: Content.reducer,
blogCatalog: BlogCatalog.reducer, blogCatalog: BlogCatalog.reducer,
blogItem: BlogItem.reducer,
shopCatalog: ShopCatalog.reducer shopCatalog: ShopCatalog.reducer
} }

View File

@ -17,7 +17,7 @@ interface ReceiveAction extends GetBlogCatalogResponseModel {
type: 'RECEIVE_BLOG_CATALOG' type: 'RECEIVE_BLOG_CATALOG'
} }
type KnownAction = RequestAction | ReceiveAction; type KnownAction = RequestAction | ReceiveAction
export const actionCreators = { export const actionCreators = {
requestBlogCatalog: (props?: GetBlogCatalogRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => { requestBlogCatalog: (props?: GetBlogCatalogRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {

View File

@ -0,0 +1,116 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { GetBlogItemRequestModel } from '../../models/requests'
import { GetBlogItemResponseModel } from '../../models/responses'
import { Get } from '../../restClient'
export interface BlogItemState extends GetBlogItemResponseModel {
isLoading: boolean
}
interface RequestAction extends GetBlogItemRequestModel {
type: 'REQUEST_BLOG_ITEM'
}
interface ReceiveAction extends GetBlogItemResponseModel {
type: 'RECEIVE_BLOG_ITEM'
}
type KnownAction = RequestAction | ReceiveAction
export const actionCreators = {
requestBlogItem: (props: GetBlogItemRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
const apiUrl = 'https://localhost:7151/api/BlogItem'
Get<Promise<GetBlogItemResponseModel>>(apiUrl, props)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_BLOG_ITEM', ...data })
})
console.log(getState().blogItem)
dispatch({ type: 'REQUEST_BLOG_ITEM', slug: props.slug })
}
}
const unloadedState: BlogItemState = {
comments: [
{
author: {
id: "",
nickName: "Commenter Name 1",
image: {
src: "https://dummyimage.com/50x50/ced4da/6c757d.jpg",
alt: "..."
}
},
comment: "If you're going to lead a space frontier, it has to be government; it'll never be private enterprise. Because the space frontier is dangerous, and it's expensive, and it has unquantified risks.",
responses: [
{
author: {
id: "",
nickName: "Commenter Name 4",
image: {
src: "https://dummyimage.com/50x50/ced4da/6c757d.jpg",
alt: "..."
}
},
comment: "And under those conditions, you cannot establish a capital-market evaluation of that enterprise. You can't get investors."
},
{
author: {
id: "",
nickName: "Commenter Name 3",
image: {
src: "https://dummyimage.com/50x50/ced4da/6c757d.jpg",
alt: "..."
}
},
comment: "When you put money directly to a problem, it makes a good headline."
}
]
},
{
author: {
id: "",
nickName: "Commenter Name 2",
image: {
src: "https://dummyimage.com/50x50/ced4da/6c757d.jpg",
alt: "..."
}
},
comment: "When I look at the universe and all the ways the universe wants to kill us, I find it hard to reconcile that with statements of beneficence."
}
],
isLoading: false
}
export const reducer: Reducer<BlogItemState> = (state: BlogItemState | undefined, incomingAction: Action): BlogItemState => {
if (state === undefined) {
return unloadedState
}
const action = incomingAction as KnownAction
switch (action.type) {
case 'REQUEST_BLOG_ITEM':
return {
...state,
isLoading: true
}
case 'RECEIVE_BLOG_ITEM':
return {
...action,
isLoading: false
}
}
return state
}