diff --git a/clientapp/src/App.tsx b/clientapp/src/App.tsx index cc5a6b9..4abe20a 100644 --- a/clientapp/src/App.tsx +++ b/clientapp/src/App.tsx @@ -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 ? {page} : page } + + return {Array.isArray(route.childRoutes) ? NestedRoutes(route.childRoutes, tag) : ''} }) } @@ -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 <> - { 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) : ''} } diff --git a/clientapp/src/components/FeatherRating/index.tsx b/clientapp/src/components/FeatherRating/index.tsx index 68ef0cc..397d4f4 100644 --- a/clientapp/src/components/FeatherRating/index.tsx +++ b/clientapp/src/components/FeatherRating/index.tsx @@ -4,7 +4,7 @@ import { FeatherIcon } from '../FeatherIcons' import { ICreateIconProps, ICreateIconResponse, IFeatherRating } from './interfaces' const FeatherRating: FC = ({ - value, + value = 0, icons = { complete: { icon: "star", color: '#ffbf00' }, half: { icon: "star", color: '#ffdf80' }, diff --git a/clientapp/src/controllers/blogCatalog.ts b/clientapp/src/controllers/blogCatalog.ts new file mode 100644 index 0000000..e9d0131 --- /dev/null +++ b/clientapp/src/controllers/blogCatalog.ts @@ -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 => await Get>(apiUrl, props) + +export { + GetBlogCatalog +} \ No newline at end of file diff --git a/clientapp/src/controllers/blogItem.ts b/clientapp/src/controllers/blogItem.ts new file mode 100644 index 0000000..0148c0c --- /dev/null +++ b/clientapp/src/controllers/blogItem.ts @@ -0,0 +1,3 @@ +import { IBlogItemModel, ICategoryModel } from "../models" + +const apiUrl = 'https://localhost:59018/api/Blog' \ No newline at end of file diff --git a/clientapp/src/controllers/shopCatalog.ts b/clientapp/src/controllers/shopCatalog.ts new file mode 100644 index 0000000..8bcaaf6 --- /dev/null +++ b/clientapp/src/controllers/shopCatalog.ts @@ -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 => await Get>(apiUrl, props) + +export { + GetShopCatalog +} \ No newline at end of file diff --git a/clientapp/src/controllers/staticContent.ts b/clientapp/src/controllers/staticContent.ts new file mode 100644 index 0000000..325298e --- /dev/null +++ b/clientapp/src/controllers/staticContent.ts @@ -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 => await Get>(apiUrl, props) + +export { + GetStaticContent +} \ No newline at end of file diff --git a/clientapp/src/httpQueries/blog.ts b/clientapp/src/httpQueries/blog.ts deleted file mode 100644 index 5df8e2e..0000000 --- a/clientapp/src/httpQueries/blog.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { IBlogItemModel, ICategoryModel, IFetchResult } from "./models" - -const apiUrl = 'https://localhost:59018/api/Blog' \ No newline at end of file diff --git a/clientapp/src/httpQueries/blogs.ts b/clientapp/src/httpQueries/blogs.ts deleted file mode 100644 index f4538fc..0000000 --- a/clientapp/src/httpQueries/blogs.ts +++ /dev/null @@ -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 => { - 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 -} \ No newline at end of file diff --git a/clientapp/src/httpQueries/shopCatalog.ts b/clientapp/src/httpQueries/shopCatalog.ts deleted file mode 100644 index b15e16d..0000000 --- a/clientapp/src/httpQueries/shopCatalog.ts +++ /dev/null @@ -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 { - -} \ No newline at end of file diff --git a/clientapp/src/interfaces/index.ts b/clientapp/src/interfaces/index.ts deleted file mode 100644 index cc44dbd..0000000 --- a/clientapp/src/interfaces/index.ts +++ /dev/null @@ -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 -} - - diff --git a/clientapp/src/layouts/admin/NavMenu/index.tsx b/clientapp/src/layouts/admin/NavMenu/index.tsx index 73161b3..3430ac7 100644 --- a/clientapp/src/layouts/admin/NavMenu/index.tsx +++ b/clientapp/src/layouts/admin/NavMenu/index.tsx @@ -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 = (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 = (props: INavMenu) => { } return
- + {/* @@ -43,7 +42,7 @@ const NavMenu : FC = (props: INavMenu) => { })} - + */}
} diff --git a/clientapp/src/layouts/admin/SideMenu/index.tsx b/clientapp/src/layouts/admin/SideMenu/index.tsx index acbabdd..37b3c7e 100644 --- a/clientapp/src/layouts/admin/SideMenu/index.tsx +++ b/clientapp/src/layouts/admin/SideMenu/index.tsx @@ -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 = (props: ISubMenu) => { - const { icon, title, items } = props + //const { icon, title, items } = props const [collapsed, setCollapsed] = useState(true) const toggle = () => setCollapsed(!collapsed) - return ( -
- - - {icon ? : ''} - {title} - - - - {items.map((item: ISubMenuItem, index: number) => ( - - - {item.icon ? : ''} - {item.title} - - - ))} - -
- ) + return
+ {/* + + {icon ? : ''} + {title} + + + + {items.map((item: ISubMenuItem, index: number) => ( + + + {item.icon ? : ''} + {item.title} + + + ))} + */} +
} const SideMenu : FC = () => { - let { sideMenu = [] } = useSelector((state: IReduxState) => state.settings) + //let { sideMenu = [] } = useSelector((state: IReduxState) => state.settings) return
- */}
} diff --git a/clientapp/src/layouts/interfaces.tsx b/clientapp/src/layouts/interfaces.tsx index 6f16a3b..6394078 100644 --- a/clientapp/src/layouts/interfaces.tsx +++ b/clientapp/src/layouts/interfaces.tsx @@ -1,4 +1,3 @@ -import { ISettingsState } from "../store/reducers/Settings" export interface ILayout { children?: React.ReactNode diff --git a/clientapp/src/layouts/public/Footer/index.tsx b/clientapp/src/layouts/public/Footer/index.tsx index 2f66c81..edff5da 100644 --- a/clientapp/src/layouts/public/Footer/index.tsx +++ b/clientapp/src/layouts/public/Footer/index.tsx @@ -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
-

Copyright © {siteName} {(new Date).getFullYear()}

+ {/*

Copyright © {siteName} {(new Date).getFullYear()}

*/}
} diff --git a/clientapp/src/layouts/public/NavMenu/index.tsx b/clientapp/src/layouts/public/NavMenu/index.tsx index 576830f..9b73207 100644 --- a/clientapp/src/layouts/public/NavMenu/index.tsx +++ b/clientapp/src/layouts/public/NavMenu/index.tsx @@ -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
+ {/** {siteName}
    - {topMenu.map((item: IMenuItem, index: number) => { + {topMenu.map((item: IMenuItemModel, index: number) => { return {item.icon ? : ''} @@ -42,6 +47,7 @@ const NavMenu : FC = () => { + */}
} diff --git a/clientapp/src/httpQueries/models.ts b/clientapp/src/models/index.ts similarity index 75% rename from clientapp/src/httpQueries/models.ts rename to clientapp/src/models/index.ts index 83ca426..003518e 100644 --- a/clientapp/src/httpQueries/models.ts +++ b/clientapp/src/models/index.ts @@ -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 } \ No newline at end of file diff --git a/clientapp/src/pages/Blog/Catalog/index.tsx b/clientapp/src/pages/Blog/Catalog/index.tsx index b56f9a3..bca91aa 100644 --- a/clientapp/src/pages/Blog/Catalog/index.tsx +++ b/clientapp/src/pages/Blog/Catalog/index.tsx @@ -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 = (props) => { } -const BlogPagination: FC = (props) => { - const { items } = props +const BlogItemsPagination: FC = (props) => { + const { items, currentPage, totalPages } = props return <> {items.map((item, index) => @@ -51,7 +51,7 @@ const BlogPagination: FC = (props) => {
{item.created}

{item.title}

{item.shortText}

- Read more → + Read more → @@ -72,13 +72,11 @@ const BlogPagination: FC = (props) => { } - - const BlogCatalog = () => { - const [state, setState] = useState() + const [state, setState] = useState() useEffect(() => { - GetBlogs().then(response => { + GetBlogCatalog().then(response => { setState(response) }) }, []) @@ -98,7 +96,7 @@ const BlogCatalog = () => { {state?.featuredBlog ? : ''} - {state?.blogItemsPagination ? : '' } + {state?.blogItemsPagination ? : '' } diff --git a/clientapp/src/pages/Blog/SideWidgets/index.tsx b/clientapp/src/pages/Blog/SideWidgets/index.tsx index 1988137..8b1548a 100644 --- a/clientapp/src/pages/Blog/SideWidgets/index.tsx +++ b/clientapp/src/pages/Blog/SideWidgets/index.tsx @@ -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 diff --git a/clientapp/src/pages/Home/index.tsx b/clientapp/src/pages/Home/index.tsx index 9880905..8f13ab4 100644 --- a/clientapp/src/pages/Home/index.tsx +++ b/clientapp/src/pages/Home/index.tsx @@ -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 = (props) => { const { title, text } = props @@ -36,15 +75,7 @@ const TitleSection : FC = (props) => { } -interface IFeaturesSectionItem { - icon: string, - title: string, - text: string -} -interface IFeaturesSection { - title: string, - items: IFeaturesSectionItem [], -} + const FeaturesSection: FC = (props) => { const { title, items } = props @@ -72,10 +103,7 @@ const FeaturesSection: FC = (props) => { } -interface ITestimonialsSection { - text: string, - image: IImage -} + const TestimonialsSection: FC = (props) => { const { text, image } = props @@ -98,28 +126,12 @@ const TestimonialsSection: FC = (props) => { } -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 = (props) => { + + +const FromOurBlogSection: FC = (props) => { const { title, text, items } = props return
@@ -148,8 +160,8 @@ const FromOurBlogSection: FC = (props) => {
-
{item.author.name}
-
{item.date} · {item.readTime}
+
{item.author.nickName}
+
{item.created} · {item.readTime}
@@ -161,11 +173,7 @@ const FromOurBlogSection: FC = (props) => {
} -interface ICallToActionSection { - title: string, - text: string, - privacyDisclaimer: string -} + const CallToActionSection: FC = (props) => { const { title, text, privacyDisclaimer } = props @@ -191,120 +199,16 @@ const CallToActionSection: FC = (props) => { } const Home = () => { - const titleSection = { - title: "Hello, world!", - text: ` -

Welcome to your new single-page application, built with:

- - ` - } + 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 Counter then Back to return here." - }, - { - icon: "server", - title: "Development server integration", - text: "In development mode, the development server from create-react-app 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 dotnet publish configuration produces minified, efficiently bundled JavaScript files." - } - ] - } - - const testimonialsSection = { - text: "The ClientApp subdirectory is a standard React application based on the create-react-app template. If you open a command prompt in that directory, you can run yarn commands such as yarn test or yarn install.", - 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 <> - - - - - + { page?.titleSection ? : '' } + { page?.featuresSection ? : '' } + { page?.testimonialsSection ? : '' } + { page?.featuredBlogsSection ? : '' } + { page?.callToActionSection ? :'' } } diff --git a/clientapp/src/pages/Shop/Catalog/index.tsx b/clientapp/src/pages/Shop/Catalog/index.tsx index 86c1949..51e1937 100644 --- a/clientapp/src/pages/Shop/Catalog/index.tsx +++ b/clientapp/src/pages/Shop/Catalog/index.tsx @@ -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 = (props) => { + const { items, currentPage, totalPages } = props + + return
+ + + {items.map((item, index) => + +
{item.badge}
+ + + + + + +
+
{item.title}
+ + + + {item.newPrice + ? <>{item.price} {item.newPrice} + : item.price} + +
+
+ + + +
+ + + )} +
+
+
+} + + const ShopCatalog = () => { const items = [ @@ -54,6 +99,14 @@ const ShopCatalog = () => { } ] + const [state, setState] = useState() + + useEffect(() => { + GetShopCatalog().then(response => { + setState(response) + }) + }, []) + return <>
@@ -65,39 +118,7 @@ const ShopCatalog = () => {
-
- - - {items.map((item, index) => - -
Sale
- - - - -
-
Fancy Product
- - - - {item.newPrice - ? <>{item.price} {item.newPrice} - : item.price} - -
-
- - - -
- - - )} -
-
-
+ {state?.shopItemsPagination ? : ''} } diff --git a/clientapp/src/pages/index.tsx b/clientapp/src/pages/index.tsx index 93d514b..07f3b11 100644 --- a/clientapp/src/pages/index.tsx +++ b/clientapp/src/pages/index.tsx @@ -12,7 +12,7 @@ import { ShopCatalog, ShopItem } from './Shop' import { BlogCatalog, BlogItem } from './Blog' interface IPages { - [key: string]: React.FC; + [key: string]: FC; } const pages: IPages = { diff --git a/clientapp/src/restClient.ts b/clientapp/src/restClient.ts new file mode 100644 index 0000000..1adfb77 --- /dev/null +++ b/clientapp/src/restClient.ts @@ -0,0 +1,62 @@ + + +export interface IRequest { + [key: string]: string | undefined +} + +interface IFetchResult { + status: number, + text: string +} + +const Post = () => { + +} + +const Get = async (apiUrl: string, props?: IRequest): Promise => { + 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 +} + + + diff --git a/clientapp/src/store/index.ts b/clientapp/src/store/index.ts index 916c85c..b1a22e6 100644 --- a/clientapp/src/store/index.ts +++ b/clientapp/src/store/index.ts @@ -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 diff --git a/clientapp/src/store/reducers/Content.ts b/clientapp/src/store/reducers/Content.ts new file mode 100644 index 0000000..52c7d2f --- /dev/null +++ b/clientapp/src/store/reducers/Content.ts @@ -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 => 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 = (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 +} \ No newline at end of file diff --git a/clientapp/src/store/reducers/Settings.ts b/clientapp/src/store/reducers/Settings.ts deleted file mode 100644 index efe0ad9..0000000 --- a/clientapp/src/store/reducers/Settings.ts +++ /dev/null @@ -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 => (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 = (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 -} \ No newline at end of file diff --git a/webapi/WeatherForecast/Controllers/BlogsController.cs b/webapi/WeatherForecast/Controllers/BlogCatalogController.cs similarity index 88% rename from webapi/WeatherForecast/Controllers/BlogsController.cs rename to webapi/WeatherForecast/Controllers/BlogCatalogController.cs index 42e5682..2d6d909 100644 --- a/webapi/WeatherForecast/Controllers/BlogsController.cs +++ b/webapi/WeatherForecast/Controllers/BlogCatalogController.cs @@ -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 _logger; - public BlogsController(ILogger logger) { + public BlogCatalogController(ILogger 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 { "react", "redux", "webapi" }, + ReadTime = 10, Likes = 200, - Tags = new List { "react", "redux", "webapi" } }; var blogModels = new List(); @@ -64,7 +66,7 @@ public class BlogsController : ControllerBase { blogModels.Add(blogItemModel); } - var blogResponse = new GetBlogsResponse { + var blogCatalogResponse = new GetBlogCatalogResponse { FeaturedBlog = blogItemModel, BlogItemsPagination = new PaginationModel { CurrentPage = currentPage, @@ -101,7 +103,7 @@ public class BlogsController : ControllerBase { - return Ok(blogResponse); + return Ok(blogCatalogResponse); } } diff --git a/webapi/WeatherForecast/Controllers/ShopCatalog.cs b/webapi/WeatherForecast/Controllers/ShopCatalog.cs deleted file mode 100644 index fe4c350..0000000 --- a/webapi/WeatherForecast/Controllers/ShopCatalog.cs +++ /dev/null @@ -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 ShopItemsPagination { get; set; } -} -#endregion - -[AllowAnonymous] -[ApiController] -[Route("api/[controller]")] -public class ShopCatalog : ControllerBase { - - private readonly ILogger _logger; - - public ShopCatalog(ILogger logger) { - _logger = logger; - } - - /// - /// - /// - /// - /// - /// - /// - /// - [HttpGet] - public IActionResult Get([FromQuery] Guid? category, [FromQuery] string? searchText, [FromQuery] int currentPage = 1, [FromQuery] int itemsPerPage = 4) { - var shopItemModel = new ShopItemModel { - - - }; - - - - - return Ok(); - } -} diff --git a/webapi/WeatherForecast/Controllers/ShopCatalogController.cs b/webapi/WeatherForecast/Controllers/ShopCatalogController.cs new file mode 100644 index 0000000..5e236d0 --- /dev/null +++ b/webapi/WeatherForecast/Controllers/ShopCatalogController.cs @@ -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 ShopItemsPagination { get; set; } +} +#endregion + +[AllowAnonymous] +[ApiController] +[Route("api/[controller]")] +public class ShopCatalogController : ControllerBase { + + private readonly ILogger _logger; + + public ShopCatalogController(ILogger logger) { + _logger = logger; + } + + /// + /// + /// + /// + /// + /// + /// + /// + [HttpGet] + public IActionResult Get([FromQuery] Guid? category, [FromQuery] string? searchText, [FromQuery] int currentPage = 1, [FromQuery] int itemsPerPage = 8) { + + var shopModels = new List(); + 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 { "react", "redux", "webapi" }, + + Rating = 4.5, + Price = 20, + NewPrice = 10 + }; + + shopModels.Add(shopItemModel); + } + + var shopCatalogResponse = new GetShopCatalogResponse { + ShopItemsPagination = new PaginationModel { + CurrentPage = currentPage, + TotalPages = 100, + Items = shopModels + } + }; + + + return Ok(shopCatalogResponse); + } +} diff --git a/webapi/WeatherForecast/Controllers/StaticContentController.cs b/webapi/WeatherForecast/Controllers/StaticContentController.cs new file mode 100644 index 0000000..9ddaad9 --- /dev/null +++ b/webapi/WeatherForecast/Controllers/StaticContentController.cs @@ -0,0 +1,215 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using WeatherForecast.Models; + +namespace WeatherForecast.Controllers; + +/// +/// +/// +[ApiController] +[AllowAnonymous] +[Route("api/[controller]")] +public class StaticContentController : ControllerBase { + + private readonly ILogger _logger; + + /// + /// + /// + /// + public StaticContentController( + ILogger logger + ) { + _logger = logger; + } + + /// + /// + /// + /// + [HttpGet] + public IActionResult Get([FromQuery] string? locale = "en-US") { + + var routes = new List { + new RouteModel ("/", "Home"), + new RouteModel ("/home", "Home") + }; + + var shopRoute = new RouteModel("/shop", + new List { + new RouteModel ("", "ShopCatalog"), + new RouteModel (":page", "ShopCatalog"), + new RouteModel (":page", new List { + new RouteModel (":slug", "ShopItem") + }) + }); + + var blogRoute = new RouteModel("/blog", + new List { + new RouteModel ("", "BlogCatalog"), + new RouteModel (":page", "BlogCatalog"), + new RouteModel (":page", new List { + new RouteModel (":slug", "BlogItem") + }) + }); + + routes.Add(shopRoute); + routes.Add(blogRoute); + + var demoRoutes = new List { + new RouteModel ("/counter", "Counter"), + new RouteModel ("/fetch-data", new List { + new RouteModel ("", "FetchData"), + new RouteModel (":startDateIndex", "FetchData") + }) + }; + + routes = routes.Concat(demoRoutes).ToList(); + + var adminRoutes = new List { + new RouteModel ("/admin", "AdminHome") + }; + + var serviceRoutes = new List { + new RouteModel ("/signin", "Signin"), + new RouteModel ("/signup", "Signup"), + new RouteModel ("*", "Error") + }; + + var topMenu = new List { + new MenuItemModel ("Home", "/"), + new MenuItemModel ("Shop", "/shop"), + new MenuItemModel ("Blog", "/blog"), + new MenuItemModel ("Signin", "/signin"), + new MenuItemModel ("Sognout", "/signout") + }; + + var sideMenu = new List { + new MenuItemModel ("alert-triangle", "Home", "/admin"), + new MenuItemModel ("activity", "Page", new List { + 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(); + 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 { "react", "redux", "webapi" }, + + ReadTime = 10, + Likes = 200, + }; + + blogItems.Add(blogItemModel); + } + + + var pages = new List(); + + pages.Add(new { + Id = "HomePage", + TitleSection = new { + Title = "Hello, World!", + Text = @" +

Welcome to your new single-page application, built with:

+ ", + 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 Counter then Back to return here." + }, + new { + Icon = "server", + Title = "Development server integration", + Text = "In development mode, the development server from create-react-app 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 dotnet publish configuration produces minified, efficiently bundled JavaScript files." + } + } + }, + TestimonialsSection = new { + Items = new[] { + new { + Text = "The ClientApp subdirectory is a standard React application based on the create-react-app template. If you open a command prompt in that directory, you can run yarn commands such as yarn test or yarn install.", + 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 + }); + } +} diff --git a/webapi/WeatherForecast/Models/MenuItemModel.cs b/webapi/WeatherForecast/Models/MenuItemModel.cs new file mode 100644 index 0000000..ce2ee22 --- /dev/null +++ b/webapi/WeatherForecast/Models/MenuItemModel.cs @@ -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? ChildItems { get; private set; } + + public MenuItemModel(string title, string target) { + Title = title; + Target = target; + } + + public MenuItemModel(string title, string target, List 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 childItems) : this(icon, title, target) { + ChildItems = childItems; + } + + + } +} diff --git a/webapi/WeatherForecast/Models/PostItemModel.cs b/webapi/WeatherForecast/Models/PostItemModel.cs index 7c84f96..0415e0b 100644 --- a/webapi/WeatherForecast/Models/PostItemModel.cs +++ b/webapi/WeatherForecast/Models/PostItemModel.cs @@ -1,5 +1,5 @@ namespace WeatherForecast.Models { - public class PostItemModel { + public abstract class PostItemModel { public Guid Id { get; set; } public string Slug { get; set; } diff --git a/webapi/WeatherForecast/Models/RouteModel.cs b/webapi/WeatherForecast/Models/RouteModel.cs new file mode 100644 index 0000000..b109577 --- /dev/null +++ b/webapi/WeatherForecast/Models/RouteModel.cs @@ -0,0 +1,19 @@ +namespace WeatherForecast.Models { + public class RouteModel { + public string Target { get; private set; } + public string? Component { get; private set; } + public List? 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 childRoutes) : this(target) { + ChildRoutes = childRoutes; + } + } +} diff --git a/webapi/WeatherForecast/Models/ShopItemModel.cs b/webapi/WeatherForecast/Models/ShopItemModel.cs index 674ce8f..4eefadd 100644 --- a/webapi/WeatherForecast/Models/ShopItemModel.cs +++ b/webapi/WeatherForecast/Models/ShopItemModel.cs @@ -2,9 +2,9 @@ public class ShopItemModel : PostItemModel { public List 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; } } }