(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 { Card, CardBody, CardHeader, Col, Row } from 'reactstrap'
import { CategoryModel } from '../../../models'
import { CategoryModel } from '../../models'
const Search = () => {
return <Card className="mb-4">

View File

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

View File

@ -67,3 +67,8 @@ export interface FormItemModel {
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"
export interface CallToActionSectionModel extends PageSectionModel {
@ -23,3 +23,7 @@ export interface TitleSectionModel extends PageSectionModel {
primaryLink?: 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 {
titleSection: TitleSectionModel
}
export interface BlogPageModel extends PageModel {
}

View File

@ -1,22 +1,24 @@
import { RequestModel } from "./abstractions"
export interface GetShopCatalogRequestModel {
[key: string]: string | undefined
export interface GetShopCatalogRequestModel extends RequestModel {
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
export interface GetBlogCatalogRequestModel {
[key: string]: string | undefined
export interface GetBlogCatalogRequestModel extends RequestModel {
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
export interface GetStaticContentRequestModel {
[key: string]: string | undefined
export interface GetBlogItemRequestModel extends RequestModel {
slug: string
}
export interface GetStaticContentRequestModel extends RequestModel {
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 { BlogCatalogPageModel, HomePageModel, ShopCatalogPageModel } from "./pages"
@ -8,6 +8,10 @@ export interface GetBlogCatalogResponseModel extends ResponseModel {
blogItemsPagination: PaginationModel<BlogItemModel>
}
export interface GetBlogItemResponseModel extends ResponseModel {
comments: CommentModel []
}
export interface GetShopCatalogResponseModel extends ResponseModel {
shopItemsPagination: PaginationModel<ShopItemModel>
}

View File

@ -11,7 +11,7 @@ import { dateFormat, findRoutes } from '../../../functions'
import { BlogItemModel, PaginationModel } from '../../../models'
import { ApplicationState } from '../../../store'
import { Categories, Empty, Search } from '../SideWidgets'
import { Categories, Empty, Search } from '../../../components/SideWidgets'
import { TitleSectionModel } from '../../../models/pageSections'
import { Pagination } from '../../../components/Pagination'
@ -27,14 +27,20 @@ const TitleSection: FC<TitleSectionModel> = (props) => {
</header>
}
const FeaturedBlog: FC<BlogItemModel> = (props) => {
const { id, slug, badge, image, title, shortText, author, created, readTime, likes, tags } = props
interface FeaturedBlogModel extends BlogItemModel {
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">
<CardImg top {...image} />
<CardBody className="p-4">
<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>
</Link>
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: shortText }}></p>
@ -122,7 +128,7 @@ const BlogCatalog = () => {
<Container fluid>
<Row>
<Col>
{blogCatalog?.featuredBlog ? <FeaturedBlog {...blogCatalog.featuredBlog} /> : ''}
{blogCatalog?.featuredBlog ? <FeaturedBlog path={path} currentPage={blogCatalog.blogItemsPagination.currentPage} {...blogCatalog.featuredBlog} /> : ''}
<Row>
{blogCatalog?.blogItemsPagination ? <BlogItemsPagination path={path} {...blogCatalog.blogItemsPagination} /> : '' }
</Row>

View File

@ -1,13 +1,101 @@
import React from 'react'
import { Card, CardBody, CardHeader, Col, Container, Row } from 'reactstrap'
import { Categories, Empty, Search } from '../SideWidgets'
// React
import React, { useEffect } from 'react'
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 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">
<Row>
@ -17,11 +105,13 @@ const BlogItem = () => {
<header className="mb-4">
<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>
<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>
<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">
<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>
</article>
<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>
<Comments comments={comments} />
</Col>

View File

@ -1,8 +1,7 @@
import { RequestModel } from "./models/abstractions"
export interface IRequest {
[key: string]: string | undefined
}
interface IFetchResult {
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)
if(props) {

View File

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

View File

@ -17,7 +17,7 @@ interface ReceiveAction extends GetBlogCatalogResponseModel {
type: 'RECEIVE_BLOG_CATALOG'
}
type KnownAction = RequestAction | ReceiveAction;
type KnownAction = RequestAction | ReceiveAction
export const actionCreators = {
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
}