(feat): reducers and interfaces refactoring

This commit is contained in:
Maksym Sadovnychyy 2022-05-25 22:06:14 +02:00
parent 49c262d5be
commit 9643070e86
33 changed files with 742 additions and 621 deletions

View File

@ -4,25 +4,26 @@ import { Route, Routes, useLocation } from 'react-router'
// Redux
import { useSelector, useDispatch } from 'react-redux'
import { actionCreators as settingsActionCreators } from './store/reducers/Settings'
import { actionCreators as settingsActionCreators } from './store/reducers/Content'
// Components
import { DynamicLayout } from './layouts'
import { DynamicPage } from './pages'
import { IReduxState, IRoute } from './interfaces'
import { IRouteModel } from './models'
import { ApplicationState } from './store'
interface IRouteProp {
path: string,
element?: JSX.Element
}
const NestedRoutes = (routes: IRoute[], tag: string | undefined = undefined) => {
const NestedRoutes = (routes: IRouteModel[], tag: string | undefined = undefined) => {
if(!Array.isArray(routes)) return
return routes.map((route: IRoute, index: number) => {
return routes.map((route: IRouteModel, index: number) => {
const routeProps: IRouteProp = {
path: route.path
path: route.target
}
if (route.component) {
@ -30,6 +31,8 @@ const NestedRoutes = (routes: IRoute[], tag: string | undefined = undefined) =>
routeProps.element = tag ? <DynamicLayout tag={tag}>{page}</DynamicLayout> : page
}
return <Route key={index} { ...routeProps }>{Array.isArray(route.childRoutes) ? NestedRoutes(route.childRoutes, tag) : ''}</Route>
})
}
@ -37,10 +40,10 @@ const NestedRoutes = (routes: IRoute[], tag: string | undefined = undefined) =>
const App: FC = () => {
const { pathname } = useLocation()
const dispatch = useDispatch()
const { routes, adminRoutes, serviceRoutes } = useSelector((state: IReduxState) => state.settings)
const state = useSelector((state: ApplicationState) => state.content)
useEffect(() => {
dispatch(settingsActionCreators.requestSettings())
dispatch(settingsActionCreators.requestContent())
}, [])
useEffect(() => {
@ -52,9 +55,9 @@ const App: FC = () => {
return <>
<Routes>
{ NestedRoutes(routes, 'PublicLayout') }
{ NestedRoutes(adminRoutes, 'AdminLayout') }
{ NestedRoutes(serviceRoutes) }
{state?.routes ? NestedRoutes(state.routes, 'PublicLayout') : ''}
{state?.adminRoutes ? NestedRoutes(state.adminRoutes, 'AdminLayout') : ''}
{state?.serviceRoutes ? NestedRoutes(state.serviceRoutes) : ''}
</Routes>
</>
}

View File

@ -4,7 +4,7 @@ import { FeatherIcon } from '../FeatherIcons'
import { ICreateIconProps, ICreateIconResponse, IFeatherRating } from './interfaces'
const FeatherRating: FC<IFeatherRating> = ({
value,
value = 0,
icons = {
complete: { icon: "star", color: '#ffbf00' },
half: { icon: "star", color: '#ffdf80' },

View File

@ -0,0 +1,24 @@
import { IBlogItemsPaginationModel, IBlogItemModel, ICategoryModel } from "../models"
import { Get } from "../restClient"
const apiUrl = 'https://localhost:59018/api/BlogCatalog'
export interface IGetBlogsRequest {
[key: string]: string | undefined
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
export interface IGetBlogCatalogResponse {
featuredBlog?: IBlogItemModel,
blogItemsPagination?: IBlogItemsPaginationModel,
categories?: ICategoryModel []
}
const GetBlogCatalog = async (props?: IGetBlogsRequest): Promise<IGetBlogCatalogResponse> => await Get<Promise<IGetBlogCatalogResponse>>(apiUrl, props)
export {
GetBlogCatalog
}

View File

@ -0,0 +1,3 @@
import { IBlogItemModel, ICategoryModel } from "../models"
const apiUrl = 'https://localhost:59018/api/Blog'

View File

@ -0,0 +1,22 @@
import { IShopItemsPaginationModel } from "../models"
import { Get } from "../restClient"
const apiUrl = 'https://localhost:59018/api/ShopCatalog'
export interface IGetShopCatalogRequest {
[key: string]: string | undefined
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
export interface IGetShopCatalogResponse {
shopItemsPagination?: IShopItemsPaginationModel,
}
const GetShopCatalog = async (props?: IGetShopCatalogRequest): Promise<IGetShopCatalogResponse> => await Get<Promise<IGetShopCatalogResponse>>(apiUrl, props)
export {
GetShopCatalog
}

View File

@ -0,0 +1,27 @@
import { IMenuItemModel, IPageModel, IRouteModel } from "../models"
import { Get } from "../restClient"
const apiUrl = 'https://localhost:59018/api/StaticContent'
export interface IGetStaticContentRequest {
[key: string]: string | undefined
locale?: string
}
export interface IGetStaticContetnResponse {
siteName: string,
routes: IRouteModel [],
adminRoutes?: IRouteModel [],
serviceRoutes?: IRouteModel [],
topMenu?: IMenuItemModel [],
sideMenu?: IMenuItemModel [],
pages?: IPageModel []
}
const GetStaticContent = async (props?: IGetStaticContentRequest): Promise<IGetStaticContetnResponse> => await Get<Promise<IGetStaticContetnResponse>>(apiUrl, props)
export {
GetStaticContent
}

View File

@ -1,3 +0,0 @@
import { IBlogItemModel, ICategoryModel, IFetchResult } from "./models"
const apiUrl = 'https://localhost:59018/api/Blog'

View File

@ -1,53 +0,0 @@
import { IBlogItemsPaginationModel, IBlogItemModel, ICategoryModel, IFetchResult } from "./models"
const apiUrl = 'https://localhost:59018/api/Blogs'
export interface IGetBlogsRequest {
[key: string]: string | undefined
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
export interface IGetBlogsResponse {
featuredBlog?: IBlogItemModel,
blogItemsPagination?: IBlogItemsPaginationModel,
categories?: ICategoryModel []
}
const GetBlogs = async (props?: IGetBlogsRequest): Promise<IGetBlogsResponse> => {
const url = new URL(apiUrl)
if(props) {
Object.keys(props).forEach(key => {
if (typeof(props[key]) !== undefined) {
url.searchParams.append(key, props[key] as string)
}
})
}
const requestParams = {
method: 'GET',
headers: { 'accept': 'application/json', 'content-type': 'application/json' },
}
console.log(`invoke:`, url.toString())
const fetchData = await fetch(url.toString(), requestParams)
.then(async fetchData => {
return {
status: fetchData.status,
text: await fetchData.text()
}
})
.catch(err => {
console.log(err)
})
return JSON.parse((fetchData as IFetchResult).text) as IGetBlogsResponse
}
export {
GetBlogs
}

View File

@ -1,13 +0,0 @@
const apiUrl = 'https://localhost:59018/api/Blogs'
export interface IGetBlogsRequest {
[key: string]: string | undefined
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
export interface IBlogsResponse {
}

View File

@ -1,28 +0,0 @@
import { ISettingsState } from "../store/reducers/Settings"
export interface IReduxState {
settings: ISettingsState
}
export interface IRoute {
path: string,
component?: string,
childRoutes?: IRoute[]
}
export interface ISubMenuItem {
icon?: string,
title: string,
target?: string
}
export interface IMenuItem extends ISubMenuItem {
items?: ISubMenuItem []
}
export interface IImage {
src: string,
alt: string
}

View File

@ -3,14 +3,13 @@ import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { Button, Collapse, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'
import { FeatherIcon } from '../../../components/FeatherIcons'
import { IMenuItem, IReduxState } from '../../../interfaces'
interface INavMenu {
toggleSidebar: () => void
}
const NavMenu : FC<INavMenu> = (props: INavMenu) => {
let { siteName, topMenu = [] } = useSelector((state: IReduxState) => state.settings)
//let { siteName, topMenu = [] } = useSelector((state: IReduxState) => state.settings)
const { toggleSidebar } = props
@ -25,7 +24,7 @@ const NavMenu : FC<INavMenu> = (props: INavMenu) => {
}
return <header>
<Navbar className="navbar-expand-sm navbar-toggleable-sm fixed-top border-bottom box-shadow mb-3 bg-light">
{/*<Navbar className="navbar-expand-sm navbar-toggleable-sm fixed-top border-bottom box-shadow mb-3 bg-light">
<Button color="light" onClick={toggleSidebar}>
<FeatherIcon icon="align-left" />
</Button>
@ -43,7 +42,7 @@ const NavMenu : FC<INavMenu> = (props: INavMenu) => {
})}
</ul>
</Collapse>
</Navbar>
</Navbar>*/}
</header>
}

View File

@ -4,48 +4,45 @@ import { useSelector } from 'react-redux'
import classNames from 'classnames'
import { Collapse, Nav, NavItem, NavLink } from 'reactstrap'
import { FeatherIcon } from '../../../components/FeatherIcons'
import { IMenuItem, IReduxState, ISubMenuItem } from '../../../interfaces'
import style from './scss/style.module.scss'
interface ISubMenu {
icon?: string,
title: string,
items: ISubMenuItem []
//items: ISubMenuItem []
}
const SubMenu : FC<ISubMenu> = (props: ISubMenu) => {
const { icon, title, items } = props
//const { icon, title, items } = props
const [collapsed, setCollapsed] = useState(true)
const toggle = () => setCollapsed(!collapsed)
return (
<div>
<NavItem onClick={toggle} className={classNames(style.navitem, !collapsed ? style.menuopen : '')}>
<NavLink className={classNames("dropdown-toggle", `${style.navlink}`)}>
{icon ? <FeatherIcon icon={icon}/> : ''}
<span className={style.linktitle}>{title}</span>
</NavLink>
</NavItem>
<Collapse isOpen={!collapsed} navbar className={classNames(`${style.itemsmenu}`, { "mb-1": !collapsed })}>
{items.map((item: ISubMenuItem, index: number) => (
<NavItem key={index} className={classNames(style.navitem, "pl-4")}>
<NavLink tag={Link} to={item.target} className={style.navlink}>
{item.icon ? <FeatherIcon icon={item.icon}/> : ''}
<span className={style.linktitle}>{item.title}</span>
</NavLink>
</NavItem>
))}
</Collapse>
</div>
)
return <div>
{/*<NavItem onClick={toggle} className={classNames(style.navitem, !collapsed ? style.menuopen : '')}>
<NavLink className={classNames("dropdown-toggle", `${style.navlink}`)}>
{icon ? <FeatherIcon icon={icon}/> : ''}
<span className={style.linktitle}>{title}</span>
</NavLink>
</NavItem>
<Collapse isOpen={!collapsed} navbar className={classNames(`${style.itemsmenu}`, { "mb-1": !collapsed })}>
{items.map((item: ISubMenuItem, index: number) => (
<NavItem key={index} className={classNames(style.navitem, "pl-4")}>
<NavLink tag={Link} to={item.target} className={style.navlink}>
{item.icon ? <FeatherIcon icon={item.icon}/> : ''}
<span className={style.linktitle}>{item.title}</span>
</NavLink>
</NavItem>
))}
</Collapse>*/}
</div>
}
const SideMenu : FC = () => {
let { sideMenu = [] } = useSelector((state: IReduxState) => state.settings)
//let { sideMenu = [] } = useSelector((state: IReduxState) => state.settings)
return <div className={style.sidemenu}>
<Nav vertical className="list-unstyled pb-3">
{/*<Nav vertical className="list-unstyled pb-3">
{sideMenu.map((item: IMenuItem, index: number) => {
if(item.items) {
return <SubMenu key={index} icon={item.icon} title={item.title} items={item.items} />
@ -58,7 +55,7 @@ const SideMenu : FC = () => {
</NavLink>
</NavItem>
})}
</Nav>
</Nav>*/}
</div>
}

View File

@ -1,4 +1,3 @@
import { ISettingsState } from "../store/reducers/Settings"
export interface ILayout {
children?: React.ReactNode

View File

@ -1,13 +1,12 @@
import React from 'react'
import { useSelector } from 'react-redux'
import { Container } from 'reactstrap'
import { IReduxState } from '../../../interfaces'
const Footer = () => {
let { siteName } = useSelector((state: IReduxState) => state.settings)
// let { siteName } = useSelector((state: IReduxState) => state.settings)
return <footer className="py-3 bg-dark">
<Container fluid><p className="m-0 text-center text-white">Copyright &copy; {siteName} {(new Date).getFullYear()}</p></Container>
{/*<Container fluid><p className="m-0 text-center text-white">Copyright &copy; {siteName} {(new Date).getFullYear()}</p></Container>*/}
</footer>
}

View File

@ -3,10 +3,13 @@ import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { Collapse, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'
import { FeatherIcon } from '../../../components/FeatherIcons'
import { IMenuItem, IReduxState } from '../../../interfaces'
const NavMenu : FC = () => {
let { siteName, topMenu = [] } = useSelector((state: IReduxState) => state.settings)
/*
let { siteName, topMenu = [] } = useSelector((state: IReduxState) => {
return state.settings
})
const [state, hookState] = useState({
isOpen: false
@ -17,14 +20,16 @@ const NavMenu : FC = () => {
isOpen: !state.isOpen
})
}
*/
return <header>
{/**
<Navbar className="navbar-expand-sm navbar-toggleable-sm fixed-top border-bottom box-shadow mb-3 bg-light">
<NavbarBrand href="/">{siteName}</ NavbarBrand>
<NavbarToggler onClick={toggle} className="mr-2"/>
<Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={state.isOpen} navbar>
<ul className="navbar-nav flex-grow">
{topMenu.map((item: IMenuItem, index: number) => {
{topMenu.map((item: IMenuItemModel, index: number) => {
return <NavItem key={index}>
<NavLink tag={Link} className="text-dark" to={item.target}>
{item.icon ? <FeatherIcon icon={item.icon}/> : ''}
@ -42,6 +47,7 @@ const NavMenu : FC = () => {
</button>
</form>
</Navbar>
*/}
</header>
}

View File

@ -1,8 +1,3 @@
export interface IFetchResult {
status: number,
text: string
}
export interface IImageModel {
src: string,
alt: string
@ -29,8 +24,8 @@ interface IPostItemModel {
}
export interface IBlogItemModel extends IPostItemModel {
readTime: number,
likes: number
readTime?: number,
likes?: number
}
export interface IShopItemModel extends IPostItemModel {
@ -59,4 +54,21 @@ export interface IShopItemsPaginationModel extends IPostPaginationModel {
export interface ICategoryModel {
id: string,
text: string
}
export interface IRouteModel {
target: string
component?: string
childRoutes?: IRouteModel []
}
export interface IMenuItemModel {
icon?: string,
title?: string,
target?: string
childItems?: IMenuItemModel []
}
export interface IPageModel {
id: string
}

View File

@ -4,8 +4,8 @@ import { Card, CardBody, CardFooter, CardHeader, CardImg, Col, Container, Row }
import { dateFormat } from '../../../functions'
import { GetBlogs, IGetBlogsResponse } from '../../../httpQueries/blogs'
import { IBlogItemModel, IBlogItemsPaginationModel } from '../../../httpQueries/models'
import { GetBlogCatalog, IGetBlogCatalogResponse } from '../../../controllers/blogCatalog'
import { IBlogItemModel, IBlogItemsPaginationModel } from '../../../models'
import { Categories, Empty, Search } from '../SideWidgets'
@ -38,8 +38,8 @@ const FeaturedBlog: FC<IBlogItemModel> = (props) => {
}
const BlogPagination: FC<IBlogItemsPaginationModel> = (props) => {
const { items } = props
const BlogItemsPagination: FC<IBlogItemsPaginationModel> = (props) => {
const { items, currentPage, totalPages } = props
return <>
{items.map((item, index) => <Col key={index} className="lg-6">
@ -51,7 +51,7 @@ const BlogPagination: FC<IBlogItemsPaginationModel> = (props) => {
<div className="small text-muted">{item.created}</div>
<h2 className="card-title h4">{item.title}</h2>
<p className="card-text">{item.shortText}</p>
<Link to={`${item.slug}`} className="btn btn-primary">Read more </Link>
<Link to={`${currentPage}/${item.slug}`} className="btn btn-primary">Read more </Link>
</CardBody>
</Card>
@ -72,13 +72,11 @@ const BlogPagination: FC<IBlogItemsPaginationModel> = (props) => {
</>
}
const BlogCatalog = () => {
const [state, setState] = useState<IGetBlogsResponse>()
const [state, setState] = useState<IGetBlogCatalogResponse>()
useEffect(() => {
GetBlogs().then(response => {
GetBlogCatalog().then(response => {
setState(response)
})
}, [])
@ -98,7 +96,7 @@ const BlogCatalog = () => {
<Col>
{state?.featuredBlog ? <FeaturedBlog {...state.featuredBlog} /> : ''}
<Row>
{state?.blogItemsPagination ? <BlogPagination {...state.blogItemsPagination} /> : '' }
{state?.blogItemsPagination ? <BlogItemsPagination {...state.blogItemsPagination} /> : '' }
</Row>
</Col>
<Col lg="4">

View File

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

View File

@ -1,8 +1,11 @@
import React, { FC } from 'react'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
import { FeatherIcon } from '../../components/FeatherIcons'
import { IImage } from '../../interfaces'
import { IPageModel, IBlogItemModel, IImageModel } from '../../models'
import { ApplicationState } from '../../store'
import { IContentState } from '../../store/reducers/Content'
import style from './scss/style.module.scss'
@ -11,6 +14,42 @@ interface ITitleSection {
text: string
}
interface IFeaturesSectionItem {
icon: string,
title: string,
text: string
}
interface IFeaturesSection {
title: string,
items: IFeaturesSectionItem [],
}
interface ITestimonialsSection {
text: string,
image: IImageModel
}
interface IFeaturedBlogsSection {
title: string,
text: string,
items: IBlogItemModel []
}
interface ICallToActionSection {
title: string,
text: string,
privacyDisclaimer: string
}
interface IHomePage extends IPageModel {
titleSection: ITitleSection,
featuresSection: IFeaturesSection,
testimonialsSection: ITestimonialsSection,
featuredBlogsSection: IFeaturedBlogsSection,
callToActionSection: ICallToActionSection
}
const TitleSection : FC<ITitleSection> = (props) => {
const { title, text } = props
@ -36,15 +75,7 @@ const TitleSection : FC<ITitleSection> = (props) => {
}
interface IFeaturesSectionItem {
icon: string,
title: string,
text: string
}
interface IFeaturesSection {
title: string,
items: IFeaturesSectionItem [],
}
const FeaturesSection: FC<IFeaturesSection> = (props) => {
const { title, items } = props
@ -72,10 +103,7 @@ const FeaturesSection: FC<IFeaturesSection> = (props) => {
}
interface ITestimonialsSection {
text: string,
image: IImage
}
const TestimonialsSection: FC<ITestimonialsSection> = (props) => {
const { text, image } = props
@ -98,28 +126,12 @@ const TestimonialsSection: FC<ITestimonialsSection> = (props) => {
</section>
}
interface IBlogAuthor {
name: string,
image: IImage
}
interface IBlogItem {
image: IImage,
badge: string,
title: string,
text: string,
author: IBlogAuthor,
date: string,
readTime: string
}
interface IFromOurBlogSection {
title: string,
text: string,
items: IBlogItem []
}
const FromOurBlogSection: FC<IFromOurBlogSection> = (props) => {
const FromOurBlogSection: FC<IFeaturedBlogsSection> = (props) => {
const { title, text, items } = props
return <section className="py-5">
@ -148,8 +160,8 @@ const FromOurBlogSection: FC<IFromOurBlogSection> = (props) => {
<div className="d-flex align-items-center">
<img className="rounded-circle me-3" {...item.author.image} />
<div className="small">
<div className="fw-bold">{item.author.name}</div>
<div className="text-muted">{item.date} &middot; {item.readTime}</div>
<div className="fw-bold">{item.author.nickName}</div>
<div className="text-muted">{item.created} &middot; {item.readTime}</div>
</div>
</div>
</div>
@ -161,11 +173,7 @@ const FromOurBlogSection: FC<IFromOurBlogSection> = (props) => {
</section>
}
interface ICallToActionSection {
title: string,
text: string,
privacyDisclaimer: string
}
const CallToActionSection: FC<ICallToActionSection> = (props) => {
const { title, text, privacyDisclaimer } = props
@ -191,120 +199,16 @@ const CallToActionSection: FC<ICallToActionSection> = (props) => {
}
const Home = () => {
const titleSection = {
title: "Hello, world!",
text: `
<p>Welcome to your new single-page application, built with:</p>
<ul>
<li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'>C#</a> for cross-platform server-side code</li>
<li><a href='https://facebook.github.io/react/'>React</a> and <a href='https://redux.js.org/'>Redux</a> for client-side code</li>
<li><a href='https://getbootstrap.com/'>Bootstrap</a>, <a href='https://reactstrap.github.io/?path=/story/home-installation--page'>Reactstrap</a> and <a href="https://feathericons.com/">Feather icons</a> for layout and styling</li>
</ul>
`
}
const state = useSelector((state: ApplicationState) => state.content)
const featuresSecton = {
title: "To help you get started, we have also set up:",
items: [
{
icon: "navigation",
title: "Client-side navigation",
text: "For example, click <em>Counter</em> then <em>Back</em> to return here."
},
{
icon: "server",
title: "Development server integration",
text: "In development mode, the development server from <code>create-react-app</code> runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file."
},
{
icon: "terminal",
title: "Efficient production builds",
text: "In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration produces minified, efficiently bundled JavaScript files."
}
]
}
const testimonialsSection = {
text: "The <code>ClientApp</code> subdirectory is a standard React application based on the <code>create-react-app</code> template. If you open a command prompt in that directory, you can run <code>yarn</code> commands such as <code>yarn test</code> or <code>yarn install</code>.",
image: {
src: "https://dummyimage.com/40x40/ced4da/6c757d",
alt: "..."
}
}
const fromOurBlogSection = {
title: "From our blog",
text: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad.",
items: [
{
badge: "News",
image: {
src: "https://dummyimage.com/600x350/ced4da/6c757d",
alt: "..."
},
title: "Blog post title",
text: "Some quick example text to build on the card title and make up the bulk of the card's content.",
author: {
image: {
src: "https://dummyimage.com/40x40/ced4da/6c757d",
alt: "..."
},
name: "Kelly Rowan"
},
date: "March 12, 2022",
readTime: "6 min read"
},
{
badge: "News",
image: {
src: "https://dummyimage.com/600x350/ced4da/6c757d",
alt: "..."
},
title: "Blog post title",
text: "Some quick example text to build on the card title and make up the bulk of the card's content.",
author: {
image: {
src: "https://dummyimage.com/40x40/ced4da/6c757d",
alt: "..."
},
name: "Kelly Rowan"
},
date: "March 12, 2022",
readTime: "6 min read"
},
{
badge: "News",
image: {
src: "https://dummyimage.com/600x350/ced4da/6c757d",
alt: "..."
},
title: "Blog post title",
text: "Some quick example text to build on the card title and make up the bulk of the card's content.",
author: {
image: {
src: "https://dummyimage.com/40x40/ced4da/6c757d",
alt: "..."
},
name: "Kelly Rowan"
},
date: "March 12, 2022",
readTime: "6 min read"
}
]
}
const callToActionSection = {
title: "New products, delivered to you.",
text: "Sign up for our newsletter for the latest updates.",
privacyDisclaimer: "We care about privacy, and will never share your data."
}
const page = state?.pages?.filter(x => x.id == "HomePage").shift() as IHomePage
return <>
<TitleSection {...titleSection}/>
<FeaturesSection {...featuresSecton} />
<TestimonialsSection {...testimonialsSection} />
<FromOurBlogSection {...fromOurBlogSection} />
<CallToActionSection {...callToActionSection} />
{ page?.titleSection ? <TitleSection {...page.titleSection}/> : '' }
{ page?.featuresSection ? <FeaturesSection {...page.featuresSection} /> : '' }
{ page?.testimonialsSection ? <TestimonialsSection {...page.testimonialsSection} /> : '' }
{ page?.featuredBlogsSection ? <FromOurBlogSection {...page.featuredBlogsSection} /> : '' }
{ page?.callToActionSection ? <CallToActionSection {...page.callToActionSection} /> :'' }
</>
}

View File

@ -1,9 +1,54 @@
import * as React from 'react'
import React, { FC, useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
import { FeatherRating } from '../../../components/FeatherRating'
import { IShopItemsPaginationModel } from '../../../models'
import { IGetShopCatalogResponse, GetShopCatalog } from '../../../controllers/shopCatalog'
const ShopItemsPagination: FC<IShopItemsPaginationModel> = (props) => {
const { items, currentPage, totalPages } = props
return <section className="py-5">
<Container fluid className="px-4 px-lg-5 mt-5">
<Row className="gx-4 gx-lg-5 row-cols-2 row-cols-md-3 row-cols-xl-4 justify-content-center">
{items.map((item, index) => <Col key={index} className="mb-5">
<Card className="h-100">
<div className="badge bg-dark text-white position-absolute" style={{top: "0.5rem", right: "0.5rem"}}>{item.badge}</div>
<Link to={`${currentPage}/${item.slug}`}>
<CardImg top {...item.image} />
</Link>
<CardBody>
<div className="text-center">
<h5 className="fw-bolder">{item.title}</h5>
<FeatherRating {...{
value: item?.rating ? item.rating : 0
}} />
{item.newPrice
? <><span className="text-muted text-decoration-line-through">{item.price}</span> {item.newPrice}</>
: item.price}
</div>
</CardBody>
<CardFooter className="p-4 pt-0 border-top-0 bg-transparent">
<div className="text-center"><a className="btn btn-outline-dark mt-auto" href="#">Add to cart</a></div>
</CardFooter>
</Card>
</Col>
)}
</Row>
</Container>
</section>
}
const ShopCatalog = () => {
const items = [
@ -54,6 +99,14 @@ const ShopCatalog = () => {
}
]
const [state, setState] = useState<IGetShopCatalogResponse>()
useEffect(() => {
GetShopCatalog().then(response => {
setState(response)
})
}, [])
return <>
<header className="bg-dark py-5">
<Container fluid className="px-4 px-lg-5 my-5">
@ -65,39 +118,7 @@ const ShopCatalog = () => {
</Container>
</header>
<section className="py-5">
<Container fluid className="px-4 px-lg-5 mt-5">
<Row className="gx-4 gx-lg-5 row-cols-2 row-cols-md-3 row-cols-xl-4 justify-content-center">
{items.map((item, index) => <Col key={index} className="mb-5">
<Card className="h-100">
<div className="badge bg-dark text-white position-absolute" style={{top: "0.5rem", right: "0.5rem"}}>Sale</div>
<Link to={`/shop/item/${item.id}`}><CardImg top src="https://dummyimage.com/450x300/dee2e6/6c757d.jpg" alt="..." /></Link>
<CardBody>
<div className="text-center">
<h5 className="fw-bolder">Fancy Product</h5>
<FeatherRating {...{
value: item.rating
}} />
{item.newPrice
? <><span className="text-muted text-decoration-line-through">{item.price}</span> {item.newPrice}</>
: item.price}
</div>
</CardBody>
<CardFooter className="p-4 pt-0 border-top-0 bg-transparent">
<div className="text-center"><a className="btn btn-outline-dark mt-auto" href="#">Add to cart</a></div>
</CardFooter>
</Card>
</Col>
)}
</Row>
</Container>
</section>
{state?.shopItemsPagination ? <ShopItemsPagination {...state.shopItemsPagination} /> : ''}
</>
}

View File

@ -12,7 +12,7 @@ import { ShopCatalog, ShopItem } from './Shop'
import { BlogCatalog, BlogItem } from './Blog'
interface IPages {
[key: string]: React.FC<any>;
[key: string]: FC<any>;
}
const pages: IPages = {

View File

@ -0,0 +1,62 @@
export interface IRequest {
[key: string]: string | undefined
}
interface IFetchResult {
status: number,
text: string
}
const Post = () => {
}
const Get = async <TResponse>(apiUrl: string, props?: IRequest): Promise<TResponse> => {
const url = new URL(apiUrl)
if(props) {
Object.keys(props).forEach(key => {
if (typeof(props[key]) !== undefined) {
url.searchParams.append(key, props[key] as string)
}
})
}
const requestParams = {
method: 'GET',
headers: { 'accept': 'application/json', 'content-type': 'application/json' },
}
const fetchData = await fetch(url.toString(), requestParams)
.then(async fetchData => {
return {
status: fetchData.status,
text: await fetchData.text()
}
})
.catch(err => {
console.log(err)
})
return JSON.parse((fetchData as IFetchResult).text) as TResponse
}
const Put = () => {
}
const Delete = () => {
}
export {
Post,
Get,
Put,
Delete
}

View File

@ -1,12 +1,12 @@
import * as WeatherForecasts from './reducers/WeatherForecasts'
import * as Counter from './reducers/Counter'
import * as Settings from './reducers/Settings'
import * as Content from './reducers/Content'
// The top-level state object
export interface ApplicationState {
counter: Counter.CounterState | undefined
weatherForecasts: WeatherForecasts.WeatherForecastsState | undefined
settings: Settings.ISettingsState | undefined
content: Content.IContentState | undefined
}
// Whenever an action is dispatched, Redux will update each top-level application state property using
@ -15,7 +15,7 @@ export interface ApplicationState {
export const reducers = {
counter: Counter.reducer,
weatherForecasts: WeatherForecasts.reducer,
settings: Settings.reducer
content: Content.reducer
}
// This type can be used as a hint on action creators so that its 'dispatch' and 'getState' params are

View File

@ -0,0 +1,60 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '..'
import { GetStaticContent, IGetStaticContentRequest, IGetStaticContetnResponse } from '../../controllers/staticContent'
export interface IContentState extends IGetStaticContetnResponse {
isLoading: boolean
}
interface RequestAction extends IGetStaticContentRequest {
type: 'REQUEST_CONTENT'
}
interface ReceiveAction extends IGetStaticContetnResponse {
type: 'RECEIVE_CONTENT'
}
type KnownAction = RequestAction | ReceiveAction;
export const actionCreators = {
requestContent: (): AppThunkAction<KnownAction> => async (dispatch, getState) => {
dispatch({ type: 'REQUEST_CONTENT' })
var fetchData = await GetStaticContent()
console.log(fetchData)
dispatch({ type: 'RECEIVE_CONTENT', ...fetchData })
}
}
const unloadedState: IContentState = {
siteName: "MAKS-IT",
routes: [
{ target: "/", component: "Home" }
],
isLoading: false
}
export const reducer: Reducer<IContentState> = (state: IContentState | undefined, incomingAction: Action): IContentState => {
if (state === undefined) {
return unloadedState
}
const action = incomingAction as KnownAction
switch (action.type) {
case 'REQUEST_CONTENT':
return {
...state,
isLoading: true
}
case 'RECEIVE_CONTENT':
return {
...action,
isLoading: false
}
}
return state
}

View File

@ -1,211 +0,0 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { IMenuItem, IRoute } from '../../interfaces'
export interface ISettingsState {
siteName: string,
routes: IRoute [],
adminRoutes: IRoute [],
serviceRoutes: IRoute [],
sideMenu?: IMenuItem [],
topMenu?: IMenuItem [],
isLoading: boolean
}
interface RequestSettingsAction {
type: 'REQUEST_SETTINGS'
}
interface ReceiveSettingsAction {
type: 'RECEIVE_SETTINGS',
siteName: string,
routes: IRoute [],
adminRoutes: IRoute [],
serviceRoutes: IRoute [],
sideMenu?: IMenuItem [],
topMenu?: IMenuItem [],
}
type KnownAction = RequestSettingsAction | ReceiveSettingsAction;
export const actionCreators = {
requestSettings: (): AppThunkAction<KnownAction> => (dispatch, getState) => {
dispatch({ type: 'REQUEST_SETTINGS' })
const appState = getState()
const siteName = "MAKS-IT"
const routes : IRoute[] = [
{ path: "/", component: "Home" },
{ path: "/home", component: "Home" },
{ path: "/shop",
childRoutes: [
{
path: "",
component: "ShopCatalog"
},
{
path: ":page",
childRoutes: [
{
path: ":slug",
component: "ShopItem"
}
]
}
]
},
{
path: "/blog",
childRoutes: [
{
path: "",
component: "BlogCatalog"
},
{
path: ":page",
component: "BlogCatalog"
},
{
path: ":page",
childRoutes: [
{
path: ":slug",
component: "BlogItem"
}
]
}
]
}
]
const adminRoutes : IRoute [] = [
{ path: "/admin", component: "AdminHome" },
{ path: "/counter", component: "Counter" },
{ path: "/fetch-data", component: "FetchData",
childRoutes: [
{
path: ":startDateIndex",
component: "FetchData"
}
]
}
]
const serviceRoutes : IRoute[] = [
{ path: "/signin", component: "Signin" },
{ path: "/signup", component: "Signup" }
]
const sideMenu : IMenuItem [] = [
{
icon: "alert-triangle",
title: "Home",
target: "/admin"
},
{
icon: "activity",
title: "Page",
items: [
{
icon: "activity",
title: "Page 1",
target: "/Page-1",
},
{
icon: "activity",
title: "Page 2",
target: "/Page-2",
},
]
},
{
icon: "",
title: "Counter",
target: "/counter",
},
{
icon: "",
title: "Fetch data",
target: "/fetch-data"
},
]
const topMenu : IMenuItem [] = [
{
icon: "",
title: "Home",
target: "/"
},
{
icon: "",
title: "Shop",
target: "/shop",
},
{
icon: "",
title: "Blog",
target: "/blog"
},
{
icon: "",
title: "Signin",
target: "/signin"
},
{
icon: "",
title: "Signout",
target: "/signout"
}
]
dispatch({ type: 'RECEIVE_SETTINGS', siteName, routes, adminRoutes, serviceRoutes, sideMenu, topMenu })
}
}
const unloadedState: ISettingsState = {
siteName: "reactredux",
routes: [],
adminRoutes: [],
serviceRoutes: [],
sideMenu: [],
topMenu: [],
isLoading: false
}
export const reducer: Reducer<ISettingsState> = (state: ISettingsState | undefined, incomingAction: Action): ISettingsState => {
if (state === undefined) {
return unloadedState
}
const action = incomingAction as KnownAction
switch (action.type) {
case 'REQUEST_SETTINGS':
return {
siteName: state.siteName,
routes: state.routes,
adminRoutes: state.adminRoutes,
serviceRoutes: state.serviceRoutes,
sideMenu: state.sideMenu,
topMenu: state.topMenu,
isLoading: true
}
case 'RECEIVE_SETTINGS':
return {
siteName: action.siteName,
routes: action.routes,
adminRoutes: action.adminRoutes,
serviceRoutes: action.serviceRoutes,
sideMenu: action.sideMenu,
topMenu: action.topMenu,
isLoading: false
}
}
return state
}

View File

@ -9,7 +9,7 @@ using Core.Abstractions.Models;
namespace WeatherForecast.Controllers;
#region Input models
public class GetBlogsResponse : ResponseModel {
public class GetBlogCatalogResponse : ResponseModel {
public BlogItemModel FeaturedBlog { get; set; }
@ -23,11 +23,11 @@ public class GetBlogsResponse : ResponseModel {
[AllowAnonymous]
[ApiController]
[Route("api/[controller]")]
public class BlogsController : ControllerBase {
public class BlogCatalogController : ControllerBase {
private readonly ILogger<LoginController> _logger;
public BlogsController(ILogger<LoginController> logger) {
public BlogCatalogController(ILogger<LoginController> logger) {
_logger = logger;
}
@ -48,15 +48,17 @@ public class BlogsController : ControllerBase {
Badge = "news",
Title = "Blog post title",
ShortText = "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...",
Text = "",
Author = new AuthorModel {
Id = Guid.NewGuid(),
Image = new ImageModel { Src = "https://dummyimage.com/40x40/ced4da/6c757d", Alt = "..." },
NickName = "Admin"
},
Created = DateTime.UtcNow,
Tags = new List<string> { "react", "redux", "webapi" },
ReadTime = 10,
Likes = 200,
Tags = new List<string> { "react", "redux", "webapi" }
};
var blogModels = new List<BlogItemModel>();
@ -64,7 +66,7 @@ public class BlogsController : ControllerBase {
blogModels.Add(blogItemModel);
}
var blogResponse = new GetBlogsResponse {
var blogCatalogResponse = new GetBlogCatalogResponse {
FeaturedBlog = blogItemModel,
BlogItemsPagination = new PaginationModel<BlogItemModel> {
CurrentPage = currentPage,
@ -101,7 +103,7 @@ public class BlogsController : ControllerBase {
return Ok(blogResponse);
return Ok(blogCatalogResponse);
}
}

View File

@ -1,47 +0,0 @@
using Core.Abstractions.Models;
using Core.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WeatherForecast.Models;
namespace WeatherForecast.Controllers;
#region Response models
public class GetShopCatalogResponse : ResponseModel {
public PaginationModel<ShopItemModel> ShopItemsPagination { get; set; }
}
#endregion
[AllowAnonymous]
[ApiController]
[Route("api/[controller]")]
public class ShopCatalog : ControllerBase {
private readonly ILogger<LoginController> _logger;
public ShopCatalog(ILogger<LoginController> logger) {
_logger = logger;
}
/// <summary>
///
/// </summary>
/// <param name="category"></param>
/// <param name="searchText"></param>
/// <param name="currentPage"></param>
/// <param name="itemsPerPage"></param>
/// <returns></returns>
[HttpGet]
public IActionResult Get([FromQuery] Guid? category, [FromQuery] string? searchText, [FromQuery] int currentPage = 1, [FromQuery] int itemsPerPage = 4) {
var shopItemModel = new ShopItemModel {
};
return Ok();
}
}

View File

@ -0,0 +1,77 @@
using Core.Abstractions.Models;
using Core.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WeatherForecast.Models;
namespace WeatherForecast.Controllers;
#region Response models
public class GetShopCatalogResponse : ResponseModel {
public PaginationModel<ShopItemModel> ShopItemsPagination { get; set; }
}
#endregion
[AllowAnonymous]
[ApiController]
[Route("api/[controller]")]
public class ShopCatalogController : ControllerBase {
private readonly ILogger<LoginController> _logger;
public ShopCatalogController(ILogger<LoginController> logger) {
_logger = logger;
}
/// <summary>
///
/// </summary>
/// <param name="category"></param>
/// <param name="searchText"></param>
/// <param name="currentPage"></param>
/// <param name="itemsPerPage"></param>
/// <returns></returns>
[HttpGet]
public IActionResult Get([FromQuery] Guid? category, [FromQuery] string? searchText, [FromQuery] int currentPage = 1, [FromQuery] int itemsPerPage = 8) {
var shopModels = new List<ShopItemModel>();
for (int i = 0; i < 8; i++) {
var shopItemModel = new ShopItemModel {
Id = Guid.NewGuid(),
Slug = "shop-catalog-item",
Image = new ImageModel { Src = "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", Alt = "..." },
Badge = "sale",
Title = "Shop item title",
ShortText = "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...",
Text = "",
Author = new AuthorModel {
Id = Guid.NewGuid(),
Image = new ImageModel { Src = "https://dummyimage.com/40x40/ced4da/6c757d", Alt = "..." },
NickName = "Admin"
},
Created = DateTime.UtcNow,
Tags = new List<string> { "react", "redux", "webapi" },
Rating = 4.5,
Price = 20,
NewPrice = 10
};
shopModels.Add(shopItemModel);
}
var shopCatalogResponse = new GetShopCatalogResponse {
ShopItemsPagination = new PaginationModel<ShopItemModel> {
CurrentPage = currentPage,
TotalPages = 100,
Items = shopModels
}
};
return Ok(shopCatalogResponse);
}
}

View File

@ -0,0 +1,215 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WeatherForecast.Models;
namespace WeatherForecast.Controllers;
/// <summary>
///
/// </summary>
[ApiController]
[AllowAnonymous]
[Route("api/[controller]")]
public class StaticContentController : ControllerBase {
private readonly ILogger<StaticContentController> _logger;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
public StaticContentController(
ILogger<StaticContentController> logger
) {
_logger = logger;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
[HttpGet]
public IActionResult Get([FromQuery] string? locale = "en-US") {
var routes = new List<RouteModel> {
new RouteModel ("/", "Home"),
new RouteModel ("/home", "Home")
};
var shopRoute = new RouteModel("/shop",
new List<RouteModel> {
new RouteModel ("", "ShopCatalog"),
new RouteModel (":page", "ShopCatalog"),
new RouteModel (":page", new List<RouteModel> {
new RouteModel (":slug", "ShopItem")
})
});
var blogRoute = new RouteModel("/blog",
new List<RouteModel> {
new RouteModel ("", "BlogCatalog"),
new RouteModel (":page", "BlogCatalog"),
new RouteModel (":page", new List<RouteModel> {
new RouteModel (":slug", "BlogItem")
})
});
routes.Add(shopRoute);
routes.Add(blogRoute);
var demoRoutes = new List<RouteModel> {
new RouteModel ("/counter", "Counter"),
new RouteModel ("/fetch-data", new List<RouteModel> {
new RouteModel ("", "FetchData"),
new RouteModel (":startDateIndex", "FetchData")
})
};
routes = routes.Concat(demoRoutes).ToList();
var adminRoutes = new List<RouteModel> {
new RouteModel ("/admin", "AdminHome")
};
var serviceRoutes = new List<RouteModel> {
new RouteModel ("/signin", "Signin"),
new RouteModel ("/signup", "Signup"),
new RouteModel ("*", "Error")
};
var topMenu = new List<MenuItemModel> {
new MenuItemModel ("Home", "/"),
new MenuItemModel ("Shop", "/shop"),
new MenuItemModel ("Blog", "/blog"),
new MenuItemModel ("Signin", "/signin"),
new MenuItemModel ("Sognout", "/signout")
};
var sideMenu = new List<MenuItemModel> {
new MenuItemModel ("alert-triangle", "Home", "/admin"),
new MenuItemModel ("activity", "Page", new List<MenuItemModel> {
new MenuItemModel ("activity", "Page-1", "Page-1"),
new MenuItemModel ("activity", "Page-2", "Page-2"),
new MenuItemModel ("activity", "Page-3", "Page-3")
}),
new MenuItemModel ("Counter", "/counter"),
new MenuItemModel ("Fetch data", "/fetch-data")
};
var blogItems = new List<BlogItemModel>();
for (int i = 0; i < 3; i++) {
var blogItemModel = new BlogItemModel {
Id = Guid.NewGuid(),
Slug = "blog-post-title",
Image = new ImageModel { Src = "https://dummyimage.com/600x350/ced4da/6c757d", Alt = "..." },
Badge = "news",
Title = "Blog post title",
ShortText = "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...",
Text = "",
Author = new AuthorModel {
Id = Guid.NewGuid(),
Image = new ImageModel { Src = "https://dummyimage.com/40x40/ced4da/6c757d", Alt = "..." },
NickName = "Admin"
},
Created = DateTime.UtcNow,
Tags = new List<string> { "react", "redux", "webapi" },
ReadTime = 10,
Likes = 200,
};
blogItems.Add(blogItemModel);
}
var pages = new List<object>();
pages.Add(new {
Id = "HomePage",
TitleSection = new {
Title = "Hello, World!",
Text = @"
<p>Welcome to your new single-page application, built with:</p>
<ul>
<li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'>C#</a> for cross-platform server-side code</li>
<li><a href='https://facebook.github.io/react/'>React</a> and <a href='https://redux.js.org/'>Redux</a> for client-side code</li>
<li><a href='https://getbootstrap.com/'>Bootstrap</a>, <a href='https://reactstrap.github.io/?path=/story/home-installation--page'>Reactstrap</a> and <a href=\""https://feathericons.com/\"">Feather icons</a> for layout and styling</li>
</ul>",
Image = new ImageModel { Src = "https://dummyimage.com/600x400/343a40/6c757d", Alt = "..." },
PrimaryLink = new MenuItemModel("Get Started", "#features"),
SecondaryLink = new MenuItemModel("Learn More", "#!")
},
FeaturesSection = new {
Title = "To help you get started, we have also set up:",
Items = new[] {
new {
Icon = "navigation",
Title = "Client-side navigation",
Text = "For example, click <em>Counter</em> then <em>Back</em> to return here."
},
new {
Icon = "server",
Title = "Development server integration",
Text = "In development mode, the development server from <code>create-react-app</code> runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file."
},
new {
Icon = "terminal",
Title = "Efficient production builds",
Text = "In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration produces minified, efficiently bundled JavaScript files."
}
}
},
TestimonialsSection = new {
Items = new[] {
new {
Text = "The <code>ClientApp</code> subdirectory is a standard React application based on the <code>create-react-app</code> template. If you open a command prompt in that directory, you can run <code>yarn</code> commands such as <code>yarn test</code> or <code>yarn install</code>.",
Author = new AuthorModel {
Image = new ImageModel { Src = "https://dummyimage.com/40x40/ced4da/6c757d", Alt = "..." },
NickName = "Tom Ato/CEO, Pomodoro"
}
}
}
},
FeaturedBlogsSection = new {
Title = "From our blog",
Items = blogItems
},
CallToActionSection = new {
Title = "New products, delivered to you.",
Text = "Sign up for our newsletter for the latest updates.",
PrivacyDisclaimer = "We care about privacy, and will never share your data."
}
});
pages.Add(new {
Id = "ShopCatalog",
TitleSection = new {
Title = "Shop in style",
Text = "With this shop hompeage template"
}
});
pages.Add(new {
Id = "BlogCatalog",
TitleSection = new {
Title = "Welcome to Blog Home!",
Text = "A Bootstrap 5 starter layout for your next blog homepage"
}
});
return Ok(new {
SiteName = "MAKS-IT",
Routes = routes,
AdminRoutes = adminRoutes,
ServiceRoutes = serviceRoutes,
TopMenu = topMenu,
SideMenu = sideMenu,
Pages = pages
});
}
}

View File

@ -0,0 +1,27 @@
namespace WeatherForecast.Models {
public class MenuItemModel {
public string? Icon { get; set; }
public string? Title { get; private set; }
public string? Target { get; private set; }
public List<MenuItemModel>? ChildItems { get; private set; }
public MenuItemModel(string title, string target) {
Title = title;
Target = target;
}
public MenuItemModel(string title, string target, List<MenuItemModel> childItems) : this(title, target) {
ChildItems = childItems;
}
public MenuItemModel(string icon, string title, string target): this(title, target) {
Icon = icon;
}
public MenuItemModel(string icon, string title, string target, List<MenuItemModel> childItems) : this(icon, title, target) {
ChildItems = childItems;
}
}
}

View File

@ -1,5 +1,5 @@
namespace WeatherForecast.Models {
public class PostItemModel {
public abstract class PostItemModel {
public Guid Id { get; set; }
public string Slug { get; set; }

View File

@ -0,0 +1,19 @@
namespace WeatherForecast.Models {
public class RouteModel {
public string Target { get; private set; }
public string? Component { get; private set; }
public List<RouteModel>? ChildRoutes { get; private set; }
private RouteModel(string target) {
Target = target;
}
public RouteModel(string target, string component) : this(target) {
Component = component;
}
public RouteModel(string target, List<RouteModel> childRoutes) : this(target) {
ChildRoutes = childRoutes;
}
}
}

View File

@ -2,9 +2,9 @@
public class ShopItemModel : PostItemModel {
public List<ImageModel> Images { get; set; }
public string Sku { get; set; }
public int Rating { get; set; }
public int Price { get; set; }
public int NewPrice { get; set; }
public double Rating { get; set; }
public double Price { get; set; }
public double NewPrice { get; set; }
public int Quantity { get; set; }
}
}