(feat): redux loader
This commit is contained in:
parent
9643070e86
commit
268c1d0060
@ -9,8 +9,9 @@ import { actionCreators as settingsActionCreators } from './store/reducers/Conte
|
|||||||
// Components
|
// Components
|
||||||
import { DynamicLayout } from './layouts'
|
import { DynamicLayout } from './layouts'
|
||||||
import { DynamicPage } from './pages'
|
import { DynamicPage } from './pages'
|
||||||
import { IRouteModel } from './models'
|
import { RouteModel } from './models'
|
||||||
import { ApplicationState } from './store'
|
import { ApplicationState } from './store'
|
||||||
|
import { Loader } from './components/Loader'
|
||||||
|
|
||||||
|
|
||||||
interface IRouteProp {
|
interface IRouteProp {
|
||||||
@ -18,10 +19,10 @@ interface IRouteProp {
|
|||||||
element?: JSX.Element
|
element?: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
const NestedRoutes = (routes: IRouteModel[], tag: string | undefined = undefined) => {
|
const NestedRoutes = (routes: RouteModel[], tag: string | undefined = undefined) => {
|
||||||
if(!Array.isArray(routes)) return
|
if(!Array.isArray(routes)) return
|
||||||
|
|
||||||
return routes.map((route: IRouteModel, index: number) => {
|
return routes.map((route: RouteModel, index: number) => {
|
||||||
const routeProps: IRouteProp = {
|
const routeProps: IRouteProp = {
|
||||||
path: route.target
|
path: route.target
|
||||||
}
|
}
|
||||||
@ -37,10 +38,12 @@ const NestedRoutes = (routes: IRouteModel[], tag: string | undefined = undefined
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const App: FC = () => {
|
const App = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const state = useSelector((state: ApplicationState) => state.content)
|
|
||||||
|
const content = useSelector((state: ApplicationState) => state.content)
|
||||||
|
const loader = useSelector((state: ApplicationState) => state.loader)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(settingsActionCreators.requestContent())
|
dispatch(settingsActionCreators.requestContent())
|
||||||
@ -49,16 +52,18 @@ const App: FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: 'smooth',
|
behavior: 'auto',
|
||||||
})
|
})
|
||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Routes>
|
<Routes>
|
||||||
{state?.routes ? NestedRoutes(state.routes, 'PublicLayout') : ''}
|
{content?.routes ? NestedRoutes(content.routes, 'PublicLayout') : ''}
|
||||||
{state?.adminRoutes ? NestedRoutes(state.adminRoutes, 'AdminLayout') : ''}
|
{content?.adminRoutes ? NestedRoutes(content.adminRoutes, 'AdminLayout') : ''}
|
||||||
{state?.serviceRoutes ? NestedRoutes(state.serviceRoutes) : ''}
|
{content?.serviceRoutes ? NestedRoutes(content.serviceRoutes) : ''}
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
|
{loader ? <Loader {...loader} /> : ''}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
331
clientapp/src/components/Loader/index.tsx
Normal file
331
clientapp/src/components/Loader/index.tsx
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
import React, { FC, ReactNode, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import './scss/loaders.scss'
|
||||||
|
|
||||||
|
export interface LoaderProps {
|
||||||
|
visible: boolean,
|
||||||
|
loaderType?: string,
|
||||||
|
color?: string,
|
||||||
|
background?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Loader: FC<LoaderProps> = ({
|
||||||
|
visible = false,
|
||||||
|
loaderType = 'ballScaleMultiple',
|
||||||
|
color = '#000',
|
||||||
|
background = '#fff'
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
interface loaderItem {
|
||||||
|
loader: ReactNode,
|
||||||
|
tooltip: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
interface loadersDictionary {
|
||||||
|
[key: string]: loaderItem
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const loaders: loadersDictionary = {
|
||||||
|
ballPulse: {
|
||||||
|
loader: <div className="loader-inner ball-pulse">
|
||||||
|
{[...Array(3)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-pulse</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballGridPulse: {
|
||||||
|
loader: <div className="loader-inner ball-grid-pulse">
|
||||||
|
{[...Array(9)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-grid-pulse</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballClipRotate: {
|
||||||
|
loader: <div className="loader-inner ball-clip-rotate">
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-clip-rotate</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballClipRotatePulse: {
|
||||||
|
loader: <div className="loader-inner ball-clip-rotate-pulse">
|
||||||
|
{[...Array(2)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-clip-rotate-pulse</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
squareSpin: {
|
||||||
|
loader: <div className="loader-inner square-spin">
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>square-spin</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballClipRotateMultiple: {
|
||||||
|
loader: <div className="loader-inner ball-clip-rotate-multiple">
|
||||||
|
{[...Array(2)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-clip-rotate-multiple</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballPulseRise: {
|
||||||
|
loader: <div className="loader-inner ball-pulse-rise">
|
||||||
|
{[...Array(5)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-pulse-rise</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballRotate: {
|
||||||
|
loader: <div className="loader-inner ball-rotate">
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-rotate</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
cubeTransion: {
|
||||||
|
loader: <div className="loader-inner cube-transition">
|
||||||
|
{[...Array(2)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>cube-transition</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballZigZag: {
|
||||||
|
loader: <div className="loader-inner ball-zig-zag">
|
||||||
|
{[...Array(2)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-zig-zag</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballZigZagDeflect: {
|
||||||
|
loader: <div className="loader-inner ball-zig-zag-deflect">
|
||||||
|
{[...Array(2)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-zig-zag-deflect</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballTrianglePath: {
|
||||||
|
loader: <div className="loader-inner ball-triangle-path">
|
||||||
|
{[...Array(3)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-triangle-path</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballScale: {
|
||||||
|
loader: <div className="loader-inner ball-scale">
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-scale</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
lineScale: {
|
||||||
|
loader: <div className="loader-inner line-scale">
|
||||||
|
{[...Array(5)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>line-scale</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
lineScaleParty: {
|
||||||
|
loader: <div className="loader-inner line-scale-party">
|
||||||
|
{[...Array(4)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>line-scale-party</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballScaleMultiple: {
|
||||||
|
loader: <div className="loader-inner ball-scale-multiple">
|
||||||
|
{[...Array(3)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-scale-multiple</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballPulseSync: {
|
||||||
|
loader: <div className="loader-inner ball-pulse-sync">
|
||||||
|
{[...Array(3)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-pulse-sync</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballBeat: {
|
||||||
|
loader: <div className="loader-inner ball-beat">
|
||||||
|
{[...Array(3)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-beat</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
lineScalePulseOut: {
|
||||||
|
loader: <div className="loader-inner line-scale-pulse-out">
|
||||||
|
{[...Array(5)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>line-scale-pulse-out</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
lineScalePulseOutRapid: {
|
||||||
|
loader: <div className="loader-inner line-scale-pulse-out-rapid">
|
||||||
|
{[...Array(5)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>line-scale-pulse-out-rapid</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballScaleRipple: {
|
||||||
|
loader: <div className="loader-inner ball-scale-ripple">
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-scale-ripple</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballScaleRippleMultiple: {
|
||||||
|
loader: <div className="loader-inner ball-scale-ripple-multiple">
|
||||||
|
{[...Array(3)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-scale-ripple-multiple</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballSpinFadeLoader: {
|
||||||
|
loader: <div className="loader-inner ball-spin-fade-loader">
|
||||||
|
{[...Array(8)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-spin-fade-loader</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
lineSpinFadeLoader: {
|
||||||
|
loader: <div className="loader-inner line-spin-fade-loader">
|
||||||
|
{[...Array(8)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>line-spin-fade-loader</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
triangleSkewSpin: {
|
||||||
|
loader: <div className="loader-inner triangle-skew-spin">
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>triangle-skew-spin</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
pacman: {
|
||||||
|
loader: <div className="loader-inner pacman">
|
||||||
|
{[...Array(5)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>pacman</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
semiCircleSpin: {
|
||||||
|
loader: <div className="loader-inner semi-circle-spin">
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>semi-circle-spin</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballGridBeat: {
|
||||||
|
loader: <div className="loader-inner ball-grid-beat">
|
||||||
|
{[...Array(9)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-grid-beat</p></span>
|
||||||
|
},
|
||||||
|
|
||||||
|
ballScaleRandom: {
|
||||||
|
loader: <div className="loader-inner ball-scale-random">
|
||||||
|
{[...Array(3)].map((item, index) => <div key={index} style={{
|
||||||
|
backgroundColor: color
|
||||||
|
}}></div>)}
|
||||||
|
</div>,
|
||||||
|
tooltip: <span className="tooltip"><p>ball-scale-random</p></span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainStyle = {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '0',
|
||||||
|
left: '0',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
zIndex: '1000',
|
||||||
|
background: background
|
||||||
|
}
|
||||||
|
|
||||||
|
const fadeInStyle = {
|
||||||
|
display: 'block',
|
||||||
|
zIndex: 2147483647,
|
||||||
|
opacity: 1,
|
||||||
|
transition: 'width 0.25s, height 0.25s, opacity 0.25s 0.25s'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fadeOutStyle = {
|
||||||
|
display: 'none',
|
||||||
|
opacity: 0,
|
||||||
|
transition: 'width 0.5s, height 0.5s, opacity 0.5s 0.5s'
|
||||||
|
}
|
||||||
|
|
||||||
|
const loaderStyle = { ...mainStyle, ...(visible ? fadeInStyle : fadeOutStyle)}
|
||||||
|
|
||||||
|
return <div className="loader-show" style={loaderStyle as any}>
|
||||||
|
<div className="loader" style={{
|
||||||
|
position: 'absolute',
|
||||||
|
display: 'block',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%'
|
||||||
|
}}>
|
||||||
|
{loaders[loaderType].loader}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Loader
|
||||||
|
}
|
||||||
126
clientapp/src/components/Loader/scss/README.md
Normal file
126
clientapp/src/components/Loader/scss/README.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<h1 align="center">Loaders.css</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://img.shields.io/npm/v/loaders.css.svg?style=flat-square">
|
||||||
|
<img src="https://img.shields.io/bower/v/loaders.css.svg?style=flat-square">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Delightful and performance-focused pure css loading animations.
|
||||||
|
|
||||||
|
### What is this?
|
||||||
|
|
||||||
|
[See the demo](http://connoratherton.com/loaders)
|
||||||
|
|
||||||
|
A collection of loading animations written entirely in css.
|
||||||
|
Each animation is limited to a small subset of css properties in order
|
||||||
|
to avoid expensive painting and layout calculations.
|
||||||
|
|
||||||
|
I've posted links below to some fantastic articles that go into this
|
||||||
|
in a lot more detail.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```
|
||||||
|
bower install loaders.css
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
npm i --save loaders.css
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
##### Standard
|
||||||
|
- Include `loaders.min.css`
|
||||||
|
- Create an element and add the animation class (e.g. `<div class="loader-inner ball-pulse"></div>`)
|
||||||
|
- Insert the appropriate number of `<div>`s into that element
|
||||||
|
|
||||||
|
##### jQuery (optional)
|
||||||
|
- Include `loaders.min.css`, jQuery, and `loaders.css.js`
|
||||||
|
- Create an element and add the animation class (e.g. `<div class="loader-inner ball-pulse"></div>`)
|
||||||
|
- `loaders.js` is a simple helper to inject the correct number of div elements for each animation
|
||||||
|
- To initialise loaders that are added after page load select the div and call `loaders` on them (e.g. `$('.loader-inner').loaders()`)
|
||||||
|
- Enjoy
|
||||||
|
|
||||||
|
### Customising
|
||||||
|
|
||||||
|
##### Changing the background color
|
||||||
|
|
||||||
|
Add styles to the correct child `div` elements
|
||||||
|
|
||||||
|
``` css
|
||||||
|
.ball-grid-pulse > div {
|
||||||
|
background-color: orange;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Changing the loader size
|
||||||
|
|
||||||
|
Use a [2D Scale](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/scale) `transform`
|
||||||
|
|
||||||
|
```css
|
||||||
|
.loader-small .loader-inner {
|
||||||
|
transform: scale(0.5, 0.5);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Browser support
|
||||||
|
|
||||||
|
Check the [can I use](http://caniuse.com/#search=css-animation) [tables](http://caniuse.com/#search=css-transform).
|
||||||
|
All recent versions of the major browsers are supported and it has support back to IE9.
|
||||||
|
|
||||||
|
Note: The loaders aren't run through autoprefixer, see this [issue](https://github.com/ConnorAtherton/loaders.css/issues/18).
|
||||||
|
|
||||||
|
IE 11 | Firefox 36 | Chrome 41 | Safari 8
|
||||||
|
------ | ---------- | --------- | --------
|
||||||
|
✔ | ✔ | ✔ | ✔
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
Pull requests are welcome! Create another file in `src/animations`
|
||||||
|
and load it in `src/loader.scss`.
|
||||||
|
|
||||||
|
In a separate tab run `gulp --require coffee-script/register`. Open `demo/demo.html`
|
||||||
|
in a browser to see your animation running.
|
||||||
|
|
||||||
|
### Further research
|
||||||
|
|
||||||
|
- http://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/
|
||||||
|
- http://aerotwist.com/blog/pixels-are-expensive/
|
||||||
|
- http://www.html5rocks.com/en/tutorials/speed/high-performance-animations/
|
||||||
|
- http://frontendbabel.info/articles/webpage-rendering-101/
|
||||||
|
|
||||||
|
### Inspired by loaders.css
|
||||||
|
|
||||||
|
A few other folks have taken loaders and ported them elsewhere.
|
||||||
|
|
||||||
|
- **React** - [Jon Jaques](https://github.com/jonjaques) built a React demo you can check out [here](https://github.com/jonjaques/react-loaders)
|
||||||
|
- **Vue** - [Kirill Khoroshilov](https://github.com/Hokid) loaders wrapped into components [vue-loaders](https://github.com/Hokid/vue-loaders)
|
||||||
|
- **Angular** - [the-corman](https://github.com/the-cormoran/angular-loaders) created some directives for angular, as did [Masadow](https://github.com/Masadow) in [this pr](https://github.com/ConnorAtherton/loaders.css/pull/50)
|
||||||
|
- **Ember** - [Stanislav Romanov](https://github.com/kaermorchen) created an Ember addon [ember-cli-loaders](https://github.com/kaermorchen/ember-cli-loaders) for using Loaders.css in Ember applications
|
||||||
|
- **iOS** - [ninjaprox](https://github.com/ninjaprox/NVActivityIndicatorView) and [ontovnik](https://github.com/gontovnik/DGActivityIndicatorView)
|
||||||
|
- **Android** - [Jack Wang](https://github.com/81813780/AVLoadingIndicatorView) created a library and [technofreaky](https://github.com/technofreaky/Loaders.CSS-Android-App) created an app
|
||||||
|
|
||||||
|
### Licence
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 Connor Atherton
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
3
clientapp/src/components/Loader/scss/_functions.scss
Normal file
3
clientapp/src/components/Loader/scss/_functions.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@function delay($interval, $count, $index) {
|
||||||
|
@return ($index * $interval) - ($interval * $count);
|
||||||
|
}
|
||||||
25
clientapp/src/components/Loader/scss/_mixins.scss
Normal file
25
clientapp/src/components/Loader/scss/_mixins.scss
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@mixin global-bg() {
|
||||||
|
background-color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin global-animation() {
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin balls() {
|
||||||
|
@include global-bg();
|
||||||
|
|
||||||
|
width: $ball-size;
|
||||||
|
height: $ball-size;
|
||||||
|
border-radius: 100%;
|
||||||
|
margin: $margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin lines() {
|
||||||
|
@include global-bg();
|
||||||
|
|
||||||
|
width: $line-width;
|
||||||
|
height: $line-height;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: $margin;
|
||||||
|
}
|
||||||
6
clientapp/src/components/Loader/scss/_variables.scss
Normal file
6
clientapp/src/components/Loader/scss/_variables.scss
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
$primary-color: #fff !default;
|
||||||
|
$ball-size: 15px !default;
|
||||||
|
$margin: 2px !default;
|
||||||
|
$line-height: 35px !default;
|
||||||
|
$line-width: 4px !default;
|
||||||
|
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes ball-beat {
|
||||||
|
50% {
|
||||||
|
opacity: 0.2;
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-beat {
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
animation: ball-beat 0.7s 0s infinite linear;
|
||||||
|
|
||||||
|
&:nth-child(2n-1) {
|
||||||
|
animation-delay: -0.35s !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(180deg) scale(0.6);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-clip-rotate-multiple {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: -20px;
|
||||||
|
top: -20px;
|
||||||
|
border: 2px solid $primary-color;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 100%;
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
animation: rotate 1s 0s ease-in-out infinite;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
display: inline-block;
|
||||||
|
top: -10px;
|
||||||
|
left: -10px;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
border-color: $primary-color transparent $primary-color transparent;
|
||||||
|
animation-direction: reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(180deg) scale(0.6);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale {
|
||||||
|
30% {
|
||||||
|
transform: scale(0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-clip-rotate-pulse {
|
||||||
|
position: relative;
|
||||||
|
transform: translateY(-15px);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
background: $primary-color;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
top: 7px;
|
||||||
|
left: -7px;
|
||||||
|
animation: scale 1s 0s cubic-bezier(.09,.57,.49,.9) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid $primary-color;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
left: -16px;
|
||||||
|
top: -2px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid;
|
||||||
|
border-color: $primary-color transparent $primary-color transparent;
|
||||||
|
animation: rotate 1s 0s cubic-bezier(.09,.57,.49,.9) infinite;
|
||||||
|
animation-duration: 1s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-clip-rotate {
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
border: 2px solid $primary-color;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
background: transparent !important;
|
||||||
|
display: inline-block;
|
||||||
|
animation: rotate 0.75s 0s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes ball-grid-beat {
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin ball-grid-beat($n:9) {
|
||||||
|
@for $i from 1 through $n {
|
||||||
|
> div:nth-child(#{$i}) {
|
||||||
|
animation-delay: ((random(100) / 100) - 0.2) + s;
|
||||||
|
animation-duration: ((random(100) / 100) + 0.6) + s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-grid-beat {
|
||||||
|
@include ball-grid-beat();
|
||||||
|
width: ($ball-size * 3) + $margin * 6;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
float: left;
|
||||||
|
animation-name: ball-grid-beat;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-delay: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes ball-grid-pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(0.5);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin ball-grid-pulse($n:9) {
|
||||||
|
@for $i from 1 through $n {
|
||||||
|
> div:nth-child(#{$i}) {
|
||||||
|
animation-delay: ((random(100) / 100) - 0.2) + s;
|
||||||
|
animation-duration: ((random(100) / 100) + 0.6) + s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-grid-pulse {
|
||||||
|
@include ball-grid-pulse();
|
||||||
|
width: ($ball-size * 3) + $margin * 6;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
float: left;
|
||||||
|
animation-name: ball-grid-pulse;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-delay: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
$rise-amount: 30px;
|
||||||
|
|
||||||
|
@keyframes ball-pulse-rise-even {
|
||||||
|
0% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translateY(-$rise-amount);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(0.4);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateY($rise-amount);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
transform: scale(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ball-pulse-rise-odd {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.4);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translateY($rise-amount);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateY(-$rise-amount);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-pulse-rise {
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
animation-duration: 1s;
|
||||||
|
animation-timing-function: cubic-bezier(.15,.46,.9,.6);
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-delay: 0;
|
||||||
|
|
||||||
|
&:nth-child(2n) {
|
||||||
|
animation-name: ball-pulse-rise-even;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2n-1) {
|
||||||
|
animation-name: ball-pulse-rise-odd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes ball-pulse-round {
|
||||||
|
0%, 80%, 100% {
|
||||||
|
transform: scale(0.0);
|
||||||
|
-webkit-transform: scale(0.0);
|
||||||
|
} 40% {
|
||||||
|
transform: scale(1.0);
|
||||||
|
-webkit-transform: scale(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-pulse-round {
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
animation: ball-pulse-round 1.2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
@import '../functions';
|
||||||
|
|
||||||
|
$amount: 10px;
|
||||||
|
|
||||||
|
@keyframes ball-pulse-sync {
|
||||||
|
33% {
|
||||||
|
transform: translateY($amount);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translateY(-$amount);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin ball-pulse-sync($n: 3, $start: 1) {
|
||||||
|
@for $i from $start through $n {
|
||||||
|
> div:nth-child(#{$i}) {
|
||||||
|
animation: ball-pulse-sync 0.6s delay(0.07s, $n, $i) infinite ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-pulse-sync {
|
||||||
|
@include ball-pulse-sync();
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
@import '../functions';
|
||||||
|
|
||||||
|
@keyframes scale {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
45% {
|
||||||
|
transform: scale(0.1);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mixins should be separated out
|
||||||
|
@mixin ball-pulse($n: 3, $start: 1) {
|
||||||
|
@for $i from $start through $n {
|
||||||
|
> div:nth-child(#{$i}) {
|
||||||
|
animation: scale 0.75s delay(0.12s, $n, $i) infinite cubic-bezier(.2,.68,.18,1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-pulse {
|
||||||
|
@include ball-pulse();
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-rotate {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
animation: rotate 1s 0s cubic-bezier(.7,-.13,.22,.86) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before, &:after {
|
||||||
|
@include balls();
|
||||||
|
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
top: 0px;
|
||||||
|
left: -28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
top: 0px;
|
||||||
|
left: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
@import '../functions';
|
||||||
|
|
||||||
|
$size: 60px;
|
||||||
|
|
||||||
|
@keyframes ball-scale-multiple {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
5% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1.0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin ball-scale-multiple ($n: 3, $start: 2) {
|
||||||
|
@for $i from $start through $n {
|
||||||
|
> div:nth-child(#{$i}) {
|
||||||
|
animation-delay: delay(0.2s, $n, $i - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-scale-multiple {
|
||||||
|
@include ball-scale-multiple();
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
transform: translateY(-$size / 2);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: -30px;
|
||||||
|
top: 0px;
|
||||||
|
opacity: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: $size;
|
||||||
|
height: $size;
|
||||||
|
animation: ball-scale-multiple 1s 0s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
@import "ball-scale";
|
||||||
|
|
||||||
|
.ball-scale-random {
|
||||||
|
width: 37px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
animation: ball-scale 1s 0s ease-in-out infinite;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
margin-left: -7px;
|
||||||
|
animation: ball-scale 1s 0.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
margin-left: -2px;
|
||||||
|
margin-top: 9px;
|
||||||
|
animation: ball-scale 1s 0.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
@import '../functions';
|
||||||
|
|
||||||
|
$size: 50px;
|
||||||
|
|
||||||
|
@keyframes ball-scale-ripple-multiple {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin ball-scale-ripple-multiple ($n:3, $start:0) {
|
||||||
|
@for $i from $start through $n {
|
||||||
|
> div:nth-child(#{$i}) {
|
||||||
|
animation-delay: delay(0.2s, $n, $i - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-scale-ripple-multiple {
|
||||||
|
@include ball-scale-ripple-multiple();
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
transform: translateY(-$size / 2);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: -26px;
|
||||||
|
width: $size;
|
||||||
|
height: $size;
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 2px solid $primary-color;
|
||||||
|
animation: ball-scale-ripple-multiple 1.25s 0s infinite cubic-bezier(.21,.53,.56,.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes ball-scale-ripple {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-scale-ripple {
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 2px solid $primary-color;;
|
||||||
|
animation: ball-scale-ripple 1s 0s infinite cubic-bezier(.21,.53,.56,.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes ball-scale {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1.0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-scale {
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
height: 60px;
|
||||||
|
width: 60px;
|
||||||
|
animation: ball-scale 1s 0s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
@import '../functions';
|
||||||
|
|
||||||
|
$radius: 25px;
|
||||||
|
|
||||||
|
@keyframes ball-spin-fade-loader {
|
||||||
|
50% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(0.4);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin ball-spin-fade-loader($n:8, $start:1) {
|
||||||
|
@for $i from $start through $n {
|
||||||
|
> div:nth-child(#{$i}) {
|
||||||
|
$iter: 360 / $n;
|
||||||
|
$quarter: ($radius / 2) + ($radius / 5.5);
|
||||||
|
|
||||||
|
@if $i == 1 {
|
||||||
|
top: $radius;
|
||||||
|
left: 0;
|
||||||
|
} @else if $i == 2 {
|
||||||
|
top: $quarter;
|
||||||
|
left: $quarter;
|
||||||
|
} @else if $i == 3 {
|
||||||
|
top: 0;
|
||||||
|
left: $radius;
|
||||||
|
} @else if $i == 4 {
|
||||||
|
top: -$quarter;
|
||||||
|
left: $quarter;
|
||||||
|
} @else if $i == 5 {
|
||||||
|
top: -$radius;
|
||||||
|
left: 0;
|
||||||
|
} @else if $i == 6 {
|
||||||
|
top: -$quarter;
|
||||||
|
left: -$quarter;
|
||||||
|
} @else if $i == 7 {
|
||||||
|
top: 0;
|
||||||
|
left: -$radius;
|
||||||
|
} @else if $i == 8 {
|
||||||
|
top: $quarter;
|
||||||
|
left: -$quarter;
|
||||||
|
}
|
||||||
|
|
||||||
|
animation: ball-spin-fade-loader 1s delay(0.12s, $n, $i - 1) infinite linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-spin-fade-loader {
|
||||||
|
@include ball-spin-fade-loader();
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
top: -10px;
|
||||||
|
left: -10px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
$radius: 45px;
|
||||||
|
|
||||||
|
@keyframes ball-spin-loader {
|
||||||
|
75% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin ball-spin-loader($n:8, $start:1) {
|
||||||
|
@for $i from $start through $n {
|
||||||
|
> span:nth-child(#{$i}) {
|
||||||
|
$iter: 360 / $n;
|
||||||
|
$quarter: ($radius / 2) + ($radius / 5.5);
|
||||||
|
|
||||||
|
@if $i == 1 {
|
||||||
|
top: $radius;
|
||||||
|
left: 0;
|
||||||
|
} @else if $i == 2 {
|
||||||
|
top: $quarter;
|
||||||
|
left: $quarter;
|
||||||
|
} @else if $i == 3 {
|
||||||
|
top: 0;
|
||||||
|
left: $radius;
|
||||||
|
} @else if $i == 4 {
|
||||||
|
top: -$quarter;
|
||||||
|
left: $quarter;
|
||||||
|
} @else if $i == 5 {
|
||||||
|
top: -$radius;
|
||||||
|
left: 0;
|
||||||
|
} @else if $i == 6 {
|
||||||
|
top: -$quarter;
|
||||||
|
left: -$quarter;
|
||||||
|
} @else if $i == 7 {
|
||||||
|
top: 0;
|
||||||
|
left: -$radius;
|
||||||
|
} @else if $i == 8 {
|
||||||
|
top: $quarter;
|
||||||
|
left: -$quarter;
|
||||||
|
}
|
||||||
|
|
||||||
|
animation: ball-spin-loader 2s ($i * 0.9s) infinite linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-spin-loader {
|
||||||
|
@include ball-spin-loader();
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
$amount: 50px;
|
||||||
|
|
||||||
|
@keyframes ball-triangle-path-1 {
|
||||||
|
33% {
|
||||||
|
transform: translate($amount / 2, -$amount);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translate($amount, 0px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0px, 0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ball-triangle-path-2 {
|
||||||
|
33% {
|
||||||
|
transform: translate($amount / 2, $amount);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translate(- $amount / 2, $amount);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0px, 0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ball-triangle-path-3 {
|
||||||
|
33% {
|
||||||
|
transform: translate(-$amount, 0px);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translate(- $amount / 2, -$amount);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0px, 0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin ball-triangle-path($n:3) {
|
||||||
|
$animations: ball-triangle-path-1 ball-triangle-path-2 ball-triangle-path-3;
|
||||||
|
|
||||||
|
@for $i from 1 through $n {
|
||||||
|
> div:nth-child(#{$i}) {
|
||||||
|
animation-name: nth($animations, $i);
|
||||||
|
animation-delay: 0;
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-triangle-path {
|
||||||
|
position: relative;
|
||||||
|
@include ball-triangle-path();
|
||||||
|
transform: translate(-$amount / 1.667, -$amount / 1.333);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 1px solid $primary-color;
|
||||||
|
|
||||||
|
&:nth-of-type(1) {
|
||||||
|
top: $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-of-type(2) {
|
||||||
|
left: $amount / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-of-type(3) {
|
||||||
|
top: $amount;
|
||||||
|
left: $amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
$amount: 30px;
|
||||||
|
|
||||||
|
@keyframes ball-zig-deflect {
|
||||||
|
17% {
|
||||||
|
transform: translate(-$amount/2, -$amount);
|
||||||
|
}
|
||||||
|
34% {
|
||||||
|
transform: translate($amount/2, -$amount);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
67% {
|
||||||
|
transform: translate($amount/2, -$amount);
|
||||||
|
}
|
||||||
|
84% {
|
||||||
|
transform: translate(-$amount/2, -$amount);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ball-zag-deflect {
|
||||||
|
17% {
|
||||||
|
transform: translate($amount/2, $amount);
|
||||||
|
}
|
||||||
|
34% {
|
||||||
|
transform: translate(-$amount/2, $amount);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
67% {
|
||||||
|
transform: translate(-$amount/2, $amount);
|
||||||
|
}
|
||||||
|
84% {
|
||||||
|
transform: translate($amount/2, $amount);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-zig-zag-deflect {
|
||||||
|
position: relative;
|
||||||
|
transform: translate(-$amount / 2, -$amount / 2);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
margin-left: $amount / 2;
|
||||||
|
top: 4px;
|
||||||
|
left: -7px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
animation: ball-zig-deflect 1.5s 0s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
animation: ball-zag-deflect 1.5s 0s infinite linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
$amount: 30px;
|
||||||
|
|
||||||
|
@keyframes ball-zig {
|
||||||
|
33% {
|
||||||
|
transform: translate(-$amount/2, -$amount);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translate($amount/2, -$amount);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ball-zag {
|
||||||
|
33% {
|
||||||
|
transform: translate($amount/2, $amount);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translate(-$amount/2, $amount);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-zig-zag {
|
||||||
|
position: relative;
|
||||||
|
transform: translate(-$amount / 2, -$amount / 2);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include balls();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
margin-left: $amount / 2;
|
||||||
|
top: 4px;
|
||||||
|
left: -7px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
animation: ball-zig 0.7s 0s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
animation: ball-zag 0.7s 0s infinite linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
$amount: 50px;
|
||||||
|
$size: 10px;
|
||||||
|
|
||||||
|
@keyframes cube-transition {
|
||||||
|
25% {
|
||||||
|
transform: translateX($amount) scale(0.5) rotate(-90deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate($amount, $amount) rotate(-180deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateY($amount) scale(0.5) rotate(-270deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(-360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cube-transition {
|
||||||
|
position: relative;
|
||||||
|
transform: translate(-$amount / 2, -$amount / 2);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
width: $size;
|
||||||
|
height: $size;
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
left: -5px;
|
||||||
|
background-color: $primary-color;
|
||||||
|
animation: cube-transition 1.6s 0s infinite ease-in-out;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
animation-delay: -0.8s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes line-scale-pulse-out-rapid {
|
||||||
|
0% {
|
||||||
|
transform: scaley(1.0);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: scaley(0.3);
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
transform: scaley(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-scale-pulse-out-rapid {
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include lines();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
animation: line-scale-pulse-out-rapid 0.9s -0.5s infinite cubic-bezier(.11,.49,.38,.78);
|
||||||
|
|
||||||
|
&:nth-child(2), &:nth-child(4) {
|
||||||
|
animation-delay: -0.25s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(1), &:nth-child(5) {
|
||||||
|
animation-delay: 0s !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
@import '../functions';
|
||||||
|
|
||||||
|
@keyframes line-scale-pulse-out {
|
||||||
|
0% {
|
||||||
|
transform: scaley(1.0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scaley(0.4);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaley(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-scale-pulse-out {
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include lines();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
animation: line-scale-pulse-out 0.9s delay(0.2s, 3, 0) infinite cubic-bezier(.85,.25,.37,.85);
|
||||||
|
|
||||||
|
&:nth-child(2), &:nth-child(4) {
|
||||||
|
animation-delay: delay(0.2s, 3, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(1), &:nth-child(5) {
|
||||||
|
animation-delay: delay(0.2s, 3, 2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes line-scale-party {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
$random: 0.5;
|
||||||
|
transform: scale($random);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin line-scale-party($n:4) {
|
||||||
|
@for $i from 1 through $n {
|
||||||
|
> div:nth-child(#{$i}) {
|
||||||
|
animation-delay: ((random(100) / 100) - 0.2) + s;
|
||||||
|
animation-duration: ((random(100) / 100) + 0.3) + s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-scale-party {
|
||||||
|
@include line-scale-party();
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include lines();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
animation-name: line-scale-party;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-delay: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
@import '../functions';
|
||||||
|
|
||||||
|
@keyframes line-scale {
|
||||||
|
0% {
|
||||||
|
transform: scaley(1.0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scaley(0.4);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaley(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin line-scale($n:5) {
|
||||||
|
@for $i from 1 through $n {
|
||||||
|
> div:nth-child(#{$i}) {
|
||||||
|
animation: line-scale 1s delay(0.1s, $n, $i) infinite cubic-bezier(.2,.68,.18,1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-scale {
|
||||||
|
@include line-scale();
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include lines();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
@import '../functions';
|
||||||
|
|
||||||
|
$radius: 20px;
|
||||||
|
|
||||||
|
@keyframes line-spin-fade-loader {
|
||||||
|
50% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin line-spin-fade-loader($n:8, $start:1) {
|
||||||
|
@for $i from $start through $n {
|
||||||
|
> div:nth-child(#{$i}) {
|
||||||
|
$iter: 360 / $n;
|
||||||
|
$quarter: ($radius / 2) + ($radius / 5.5);
|
||||||
|
|
||||||
|
@if $i == 1 {
|
||||||
|
top: $radius;
|
||||||
|
left: 0;
|
||||||
|
} @else if $i == 2 {
|
||||||
|
top: $quarter;
|
||||||
|
left: $quarter;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
} @else if $i == 3 {
|
||||||
|
top: 0;
|
||||||
|
left: $radius;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
} @else if $i == 4 {
|
||||||
|
top: -$quarter;
|
||||||
|
left: $quarter;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
} @else if $i == 5 {
|
||||||
|
top: -$radius;
|
||||||
|
left: 0;
|
||||||
|
} @else if $i == 6 {
|
||||||
|
top: -$quarter;
|
||||||
|
left: -$quarter;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
} @else if $i == 7 {
|
||||||
|
top: 0;
|
||||||
|
left: -$radius;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
} @else if $i == 8 {
|
||||||
|
top: $quarter;
|
||||||
|
left: -$quarter;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
animation: line-spin-fade-loader 1.2s delay(0.12s, $n, $i) infinite ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-spin-fade-loader {
|
||||||
|
@include line-spin-fade-loader();
|
||||||
|
position: relative;
|
||||||
|
top: -10px;
|
||||||
|
left: -4px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include lines();
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 5px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
92
clientapp/src/components/Loader/scss/animations/pacman.scss
Normal file
92
clientapp/src/components/Loader/scss/animations/pacman.scss
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
@import '../functions';
|
||||||
|
|
||||||
|
$size: 25px;
|
||||||
|
|
||||||
|
@keyframes rotate_pacman_half_up {
|
||||||
|
0% {
|
||||||
|
transform:rotate(270deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform:rotate(360deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform:rotate(270deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate_pacman_half_down {
|
||||||
|
0% {
|
||||||
|
transform:rotate(90deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform:rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform:rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin pacman_design(){
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
border-right: $size solid transparent;
|
||||||
|
border-top: $size solid $primary-color;
|
||||||
|
border-left: $size solid $primary-color;
|
||||||
|
border-bottom: $size solid $primary-color;
|
||||||
|
border-radius: $size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pacman-balls {
|
||||||
|
75% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-4 * $size, -$size / 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin ball-placement($n:3, $start:0) {
|
||||||
|
@for $i from $start through $n {
|
||||||
|
> div:nth-child(#{$i + 2}) {
|
||||||
|
animation: pacman-balls 1s delay(.33s, $n, $i) infinite linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pacman {
|
||||||
|
@include ball-placement();
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> div:first-of-type {
|
||||||
|
@include pacman_design();
|
||||||
|
animation: rotate_pacman_half_up 0.5s 0s infinite;
|
||||||
|
position: relative;
|
||||||
|
left: -30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:nth-child(2) {
|
||||||
|
@include pacman_design();
|
||||||
|
animation: rotate_pacman_half_down 0.5s 0s infinite;
|
||||||
|
margin-top: -2 * $size;
|
||||||
|
position: relative;
|
||||||
|
left: -30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:nth-child(3),
|
||||||
|
> div:nth-child(4),
|
||||||
|
> div:nth-child(5),
|
||||||
|
> div:nth-child(6) {
|
||||||
|
@include balls();
|
||||||
|
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(0, -$size / 4);
|
||||||
|
top: 25px;
|
||||||
|
left: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
$size: 35px;
|
||||||
|
$pos: 30%;
|
||||||
|
|
||||||
|
@keyframes spin-rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.semi-circle-spin {
|
||||||
|
position: relative;
|
||||||
|
width: $size;
|
||||||
|
height: $size;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
position: absolute;
|
||||||
|
border-width: 0px;
|
||||||
|
border-radius: 100%;
|
||||||
|
animation: spin-rotate 0.6s 0s infinite linear;
|
||||||
|
background-image: linear-gradient(transparent 0%, transparent (100% - $pos), $primary-color $pos, $primary-color 100%);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
@keyframes square-spin {
|
||||||
|
25% {
|
||||||
|
transform: perspective(100px) rotateX(180deg) rotateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: perspective(100px) rotateX(180deg) rotateY(180deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: perspective(100px) rotateX(0) rotateY(180deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: perspective(100px) rotateX(0) rotateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.square-spin {
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: $primary-color;
|
||||||
|
animation: square-spin 3s 0s cubic-bezier(.09,.57,.49,.9) infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
@import '../variables';
|
||||||
|
@import '../mixins';
|
||||||
|
|
||||||
|
$size: 20px;
|
||||||
|
|
||||||
|
@keyframes triangle-skew-spin {
|
||||||
|
25% {
|
||||||
|
transform: perspective(100px) rotateX(180deg) rotateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: perspective(100px) rotateX(180deg) rotateY(180deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: perspective(100px) rotateX(0) rotateY(180deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: perspective(100px) rotateX(0) rotateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.triangle-skew-spin {
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include global-animation();
|
||||||
|
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: $size solid transparent;
|
||||||
|
border-right: $size solid transparent;
|
||||||
|
border-bottom: $size solid $primary-color;
|
||||||
|
animation: triangle-skew-spin 3s 0s cubic-bezier(.09,.57,.49,.9) infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
143
clientapp/src/components/Loader/scss/demo/demo.css
Normal file
143
clientapp/src/components/Loader/scss/demo/demo.css
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 16px;
|
||||||
|
background: #ed5565;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Source Sans Pro'; }
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.8em;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
margin: 0.6rem 0; }
|
||||||
|
h1 > span {
|
||||||
|
font-weight: 300; }
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.15em;
|
||||||
|
font-weight: 300;
|
||||||
|
margin: 0.3rem 0; }
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 95%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 4em auto;
|
||||||
|
opacity: 0; }
|
||||||
|
main.loaded {
|
||||||
|
transition: opacity .25s linear;
|
||||||
|
opacity: 1; }
|
||||||
|
main header {
|
||||||
|
width: 100%; }
|
||||||
|
main header > div {
|
||||||
|
width: 50%; }
|
||||||
|
main header > .left,
|
||||||
|
main header > .right {
|
||||||
|
height: 100%; }
|
||||||
|
main .loaders {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap; }
|
||||||
|
main .loaders .loader {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-basis: 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
height: 200px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
perspective: 500px; }
|
||||||
|
main .loaders .loader .tooltip {
|
||||||
|
-webkit-transition: all 200ms ease;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
-webkit-transform: translate3d(-50%, 0%, 0);
|
||||||
|
transform: translate3d(-50%, 0%, 0);
|
||||||
|
-webkit-transform-origin: 0 10px;
|
||||||
|
transform-origin: 0 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #2f2f2f;
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
left: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
padding: 4px 20px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: left;
|
||||||
|
top: 80%;
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: nowrap; }
|
||||||
|
main .loaders .loader .tooltip:before {
|
||||||
|
border: 6px solid;
|
||||||
|
border-color: transparent;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -10px;
|
||||||
|
position: absolute;
|
||||||
|
top: -12px;
|
||||||
|
width: 0; }
|
||||||
|
main .loaders .loader .tooltip:after {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -20px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px; }
|
||||||
|
main .loaders .loader .tooltip:hover {
|
||||||
|
-webkit-transform: rotateX(0deg) translate3d(-50%, -10%, 0);
|
||||||
|
transform: rotateX(0deg) translate3d(-50%, -10%, 0);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto; }
|
||||||
|
main .loaders .loader:hover .tooltip {
|
||||||
|
-webkit-transform: translate3d(-50%, -10%, 0);
|
||||||
|
transform: translate3d(-50%, -10%, 0);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util classes
|
||||||
|
*/
|
||||||
|
.left {
|
||||||
|
float: left; }
|
||||||
|
|
||||||
|
.right {
|
||||||
|
float: right; }
|
||||||
|
|
||||||
|
.cf, main header {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buttons
|
||||||
|
*/
|
||||||
|
.btn {
|
||||||
|
color: #fff;
|
||||||
|
padding: .75rem 1.25rem;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform .1s ease-out, border .1s ease-out, background-color .15s ease-out, color .1s ease-out;
|
||||||
|
margin: 2rem 0; }
|
||||||
|
.btn:hover {
|
||||||
|
transform: scale(1.01562);
|
||||||
|
background-color: #fff;
|
||||||
|
color: #ed5565; }
|
||||||
270
clientapp/src/components/Loader/scss/demo/demo.html
Normal file
270
clientapp/src/components/Loader/scss/demo/demo.html
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
<!DOCTYPE html5>
|
||||||
|
<head>
|
||||||
|
<link href="http://fonts.googleapis.com/css?family=Source+Sans+Pro:600,300" rel="stylesheet" type="text/css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="demo.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="../loaders.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<div class="left">
|
||||||
|
<h1>Loaders<span>.css</span></h1>
|
||||||
|
<h2>Delightful and performance-focused pure css loading animations.</h2>
|
||||||
|
</div>
|
||||||
|
<div class="right"><a href="https://github.com/ConnorAtherton/loaders.css" class="btn right">View on Github</a></div>
|
||||||
|
</header>
|
||||||
|
<div class="loaders">
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-pulse">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-pulse</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-grid-pulse">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-grid-pulse</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-clip-rotate">
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-clip-rotate</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-clip-rotate-pulse">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-clip-rotate-pulse</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner square-spin">
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>square-spin</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-clip-rotate-multiple">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-clip-rotate-multiple</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-pulse-rise">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-pulse-rise</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-rotate">
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-rotate</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner cube-transition">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>cube-transition</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-zig-zag">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-zig-zag</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-zig-zag-deflect">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-zig-zag-deflect</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-triangle-path">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-triangle-path</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-scale">
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-scale</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner line-scale">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>line-scale</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner line-scale-party">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>line-scale-party</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-scale-multiple">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-scale-multiple</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-pulse-sync">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-pulse-sync</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-beat">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-beat</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner line-scale-pulse-out">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>line-scale-pulse-out</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner line-scale-pulse-out-rapid">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>line-scale-pulse-out-rapid</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-scale-ripple">
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-scale-ripple</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-scale-ripple-multiple">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-scale-ripple-multiple</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-spin-fade-loader">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-spin-fade-loader</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner line-spin-fade-loader">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>line-spin-fade-loader</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner triangle-skew-spin">
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>triangle-skew-spin</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner pacman">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>pacman</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner semi-circle-spin">
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>semi-circle-spin</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-grid-beat">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-grid-beat</p></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader">
|
||||||
|
<div class="loader-inner ball-scale-random">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div><span class="tooltip">
|
||||||
|
<p>ball-scale-random</p></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
document.querySelector('main').className += 'loaded';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
268
clientapp/src/components/Loader/scss/demo/src/demo.jade
Normal file
268
clientapp/src/components/Loader/scss/demo/src/demo.jade
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
doctype html5
|
||||||
|
head
|
||||||
|
link(href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:600,300' rel='stylesheet' type='text/css')
|
||||||
|
link(rel="stylesheet", type="text/css", href="demo.css")
|
||||||
|
link(rel="stylesheet", type="text/css", href="../loaders.css")
|
||||||
|
body
|
||||||
|
main
|
||||||
|
header
|
||||||
|
.left
|
||||||
|
h1 Loaders
|
||||||
|
span .css
|
||||||
|
h2 Delightful and performance-focused pure css loading animations.
|
||||||
|
|
||||||
|
.right
|
||||||
|
a.btn.right(href="https://github.com/ConnorAtherton/loaders.css")
|
||||||
|
| View on Github
|
||||||
|
|
||||||
|
.loaders
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-pulse
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-pulse
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-grid-pulse
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-grid-pulse
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-clip-rotate
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-clip-rotate
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-clip-rotate-pulse
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-clip-rotate-pulse
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.square-spin
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p square-spin
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-clip-rotate-multiple
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-clip-rotate-multiple
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-pulse-rise
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-pulse-rise
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-rotate
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-rotate
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.cube-transition
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p cube-transition
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-zig-zag
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-zig-zag
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-zig-zag-deflect
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-zig-zag-deflect
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-triangle-path
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-triangle-path
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-scale
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-scale
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.line-scale
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p line-scale
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.line-scale-party
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p line-scale-party
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-scale-multiple
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-scale-multiple
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-pulse-sync
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-pulse-sync
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-beat
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-beat
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.line-scale-pulse-out
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p line-scale-pulse-out
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.line-scale-pulse-out-rapid
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p line-scale-pulse-out-rapid
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-scale-ripple
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-scale-ripple
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-scale-ripple-multiple
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-scale-ripple-multiple
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-spin-fade-loader
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-spin-fade-loader
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.line-spin-fade-loader
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p line-spin-fade-loader
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.triangle-skew-spin
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p triangle-skew-spin
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.pacman
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p pacman
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.semi-circle-spin
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p semi-circle-spin
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-grid-beat
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-grid-beat
|
||||||
|
|
||||||
|
.loader
|
||||||
|
.loader-inner.ball-scale-random
|
||||||
|
div
|
||||||
|
div
|
||||||
|
div
|
||||||
|
span.tooltip
|
||||||
|
p ball-scale-random
|
||||||
|
|
||||||
|
script.
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
document.querySelector('main').className += 'loaded';
|
||||||
|
});
|
||||||
187
clientapp/src/components/Loader/scss/demo/src/demo.scss
Normal file
187
clientapp/src/components/Loader/scss/demo/src/demo.scss
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
$gray: #dcdcdc;
|
||||||
|
$text: #fff;
|
||||||
|
$bg-color: #ed5565;
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 16px;
|
||||||
|
background: $bg-color;
|
||||||
|
color: $text;
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.8em;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
margin: 0.6rem 0;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.15em;
|
||||||
|
font-weight: 300;
|
||||||
|
margin: 0.3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 95%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 4em auto;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&.loaded {
|
||||||
|
transition: opacity .25s linear;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
@extend .cf;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .left,
|
||||||
|
> .right {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.loaders {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-basis: 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
height: 200px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
perspective: 500px;
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
-webkit-transition: all 200ms ease;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
-webkit-transform: translate3d(-50%, 0%, 0);
|
||||||
|
transform: translate3d(-50%, 0%, 0);
|
||||||
|
-webkit-transform-origin: 0 10px;
|
||||||
|
transform-origin: 0 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #2f2f2f;
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
left: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
padding: 4px 20px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: left;
|
||||||
|
top: 80%;
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
border: 6px solid;
|
||||||
|
border-color: transparent;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -10px;
|
||||||
|
position: absolute;
|
||||||
|
top: -12px;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -20px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
-webkit-transform: rotateX(0deg) translate3d(-50%, -10%, 0);
|
||||||
|
transform: rotateX(0deg) translate3d(-50%, -10%, 0);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .tooltip {
|
||||||
|
-webkit-transform: translate3d(-50%, -10%, 0);
|
||||||
|
transform: translate3d(-50%, -10%, 0);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util classes
|
||||||
|
*/
|
||||||
|
|
||||||
|
.left {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buttons
|
||||||
|
*/
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
color: $text;
|
||||||
|
padding: .75rem 1.25rem;
|
||||||
|
border: 2px solid $text;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform .1s ease-out, border .1s ease-out, background-color .15s ease-out, color .1s ease-out;
|
||||||
|
margin: 2rem 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.01562);
|
||||||
|
background-color: #fff;
|
||||||
|
color: $bg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
56
clientapp/src/components/Loader/scss/loaders.scss
Normal file
56
clientapp/src/components/Loader/scss/loaders.scss
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2016 Connor Atherton
|
||||||
|
*
|
||||||
|
* All animations must live in their own file
|
||||||
|
* in the animations directory and be included
|
||||||
|
* here.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Styles shared by multiple animations
|
||||||
|
*/
|
||||||
|
@import 'variables';
|
||||||
|
@import 'mixins';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dots
|
||||||
|
*/
|
||||||
|
@import 'animations/ball-pulse';
|
||||||
|
@import 'animations/ball-pulse-sync';
|
||||||
|
@import 'animations/ball-scale';
|
||||||
|
@import 'animations/ball-scale-random';
|
||||||
|
@import 'animations/ball-rotate';
|
||||||
|
@import 'animations/ball-clip-rotate';
|
||||||
|
@import 'animations/ball-clip-rotate-pulse';
|
||||||
|
@import 'animations/ball-clip-rotate-multiple';
|
||||||
|
@import 'animations/ball-scale-ripple';
|
||||||
|
@import 'animations/ball-scale-ripple-multiple';
|
||||||
|
@import 'animations/ball-beat';
|
||||||
|
@import 'animations/ball-scale-multiple';
|
||||||
|
@import 'animations/ball-triangle-path';
|
||||||
|
@import 'animations/ball-pulse-rise';
|
||||||
|
@import 'animations/ball-grid-beat';
|
||||||
|
@import 'animations/ball-grid-pulse';
|
||||||
|
@import 'animations/ball-spin-fade-loader';
|
||||||
|
@import 'animations/ball-spin-loader';
|
||||||
|
@import 'animations/ball-zig-zag';
|
||||||
|
@import 'animations/ball-zig-zag-deflect';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lines
|
||||||
|
*/
|
||||||
|
@import 'animations/line-scale';
|
||||||
|
@import 'animations/line-scale-random';
|
||||||
|
@import 'animations/line-scale-pulse-out';
|
||||||
|
@import 'animations/line-scale-pulse-out-rapid';
|
||||||
|
@import 'animations/line-spin-fade-loader';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Misc
|
||||||
|
*/
|
||||||
|
@import 'animations/triangle-skew-spin';
|
||||||
|
@import 'animations/square-spin';
|
||||||
|
@import 'animations/pacman';
|
||||||
|
@import 'animations/cube-transition';
|
||||||
|
@import 'animations/semi-circle-spin';
|
||||||
171
clientapp/src/components/Pagination/index.tsx
Normal file
171
clientapp/src/components/Pagination/index.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Pagination as ReactstrapPagination, PaginationItem, PaginationLink } from 'reactstrap'
|
||||||
|
import { findChunk, intToArray, splitInChunks } from './utils'
|
||||||
|
|
||||||
|
|
||||||
|
interface PaginationProps {
|
||||||
|
maxVisiblePages?: number,
|
||||||
|
totalPages: number,
|
||||||
|
currentPage: number,
|
||||||
|
onClick: (page: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pagination: FC<PaginationProps> = ({
|
||||||
|
maxVisiblePages = 5,
|
||||||
|
totalPages = 1,
|
||||||
|
currentPage = 1,
|
||||||
|
onClick
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
// << & >> buttons
|
||||||
|
let firstButton = <></>
|
||||||
|
if (currentPage > 1) {
|
||||||
|
firstButton = <PaginationLink first onClick={() => { onClick(1) }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastButton = <></>
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
lastButton = <PaginationLink last onClick={() => { onClick(totalPages) }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
// < & > buttons
|
||||||
|
let prevButton = <></>
|
||||||
|
if (currentPage > 1) {
|
||||||
|
prevButton = <PaginationLink previous onClick={() => { onClick(currentPage - 1) }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextButton = <></>
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
nextButton = <PaginationLink next onClick={() => { onClick(currentPage + 1) }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = splitInChunks(intToArray(totalPages), maxVisiblePages)
|
||||||
|
const chunk = findChunk(chunks, currentPage)
|
||||||
|
|
||||||
|
// ... & ... buttons
|
||||||
|
let prevChunk = <></>
|
||||||
|
let nextChunk = <></>
|
||||||
|
if (totalPages > maxVisiblePages) {
|
||||||
|
if (chunk.index > 0) {
|
||||||
|
prevChunk = <PaginationLink onClick={() => { onClick(chunks[chunk.index - 1][0]) }}>{'...'}</PaginationLink>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.index < chunks.length - 1) {
|
||||||
|
nextChunk = <PaginationLink onClick={() => { onClick(chunks[chunk.index + 1][0]) }}>{'...'}</PaginationLink>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// numbered pagination buttons
|
||||||
|
const pageButtons = []
|
||||||
|
for (let i = 0; i < chunk.items.length; i++) {
|
||||||
|
if (chunk.items[i] === currentPage) {
|
||||||
|
pageButtons.push(<PaginationItem active key={i}><PaginationLink>{chunk.items[i]}</PaginationLink></PaginationItem>)
|
||||||
|
} else {
|
||||||
|
pageButtons.push(<PaginationItem key={i}><PaginationLink onClick={() => { onClick(chunk.items[i]) }}>{chunk.items[i]}</PaginationLink></PaginationItem>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <nav aria-label="Pagination">
|
||||||
|
<hr className="my-0" />
|
||||||
|
<ReactstrapPagination listClassName="justify-content-center my-4">
|
||||||
|
<PaginationItem>{firstButton}</PaginationItem>
|
||||||
|
<PaginationItem>{prevButton}</PaginationItem>
|
||||||
|
<PaginationItem>{prevChunk}</PaginationItem>
|
||||||
|
{pageButtons}
|
||||||
|
<PaginationItem>{nextChunk}</PaginationItem>
|
||||||
|
<PaginationItem>{nextButton}</PaginationItem>
|
||||||
|
<PaginationItem>{lastButton}</PaginationItem>
|
||||||
|
</ReactstrapPagination>
|
||||||
|
</nav>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SSRPaginationProps {
|
||||||
|
maxVisiblePages?: number,
|
||||||
|
totalPages: number,
|
||||||
|
currentPage: number,
|
||||||
|
linksPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SSRPagination: FC<SSRPaginationProps> = ({
|
||||||
|
maxVisiblePages = 5,
|
||||||
|
totalPages = 1,
|
||||||
|
currentPage = 1,
|
||||||
|
linksPath
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
|
||||||
|
if (!linksPath) {
|
||||||
|
return (
|
||||||
|
<div className="pagination">
|
||||||
|
Server Side Prerendering Pagination disabled (Missing Link Path)
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// << & >> buttons
|
||||||
|
let firstButton = <></>
|
||||||
|
if (currentPage > 1) {
|
||||||
|
firstButton = <PaginationLink first href={`${linksPath}/${1}`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastButton = <></>
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
lastButton = <PaginationLink last href={`${linksPath}/${totalPages}`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
// < & > buttons
|
||||||
|
let prevButton = <></>
|
||||||
|
if (currentPage > 1) {
|
||||||
|
prevButton = <PaginationLink previous href={`${linksPath}/${currentPage - 1}`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextButton = <></>
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
nextButton = <PaginationLink next href={`${linksPath}/${currentPage + 1}`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = splitInChunks(intToArray(totalPages), maxVisiblePages)
|
||||||
|
const chunk = findChunk(chunks, currentPage)
|
||||||
|
|
||||||
|
// ... & ... buttons
|
||||||
|
let prevChunk = <></>
|
||||||
|
let nextChunk = <></>
|
||||||
|
if (totalPages > maxVisiblePages) {
|
||||||
|
if (chunk.index > 0) {
|
||||||
|
prevChunk = <PaginationLink href={`${linksPath}/${chunks[chunk.index - 1][0]}`}>{'...'}</PaginationLink>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.index < chunks.length - 1) {
|
||||||
|
nextChunk = <PaginationLink href={`${linksPath}/${chunks[chunk.index + 1][0]}`}>{'...'}</PaginationLink>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// numbered pagination buttons
|
||||||
|
const pageButtons = []
|
||||||
|
for (let i = 0; i < chunk.items.length; i++) {
|
||||||
|
if (chunk.items[i] === currentPage) {
|
||||||
|
pageButtons.push(<PaginationItem key={i} active><PaginationLink>{chunk.items[i]}</PaginationLink></PaginationItem>)
|
||||||
|
} else {
|
||||||
|
pageButtons.push(<PaginationItem key={i}><PaginationLink href={`${linksPath}/${chunk.items[i]}`}>{chunk.items[i]}</PaginationLink></PaginationItem>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <nav aria-label="Page navigation example">
|
||||||
|
<hr className="my-0" />
|
||||||
|
<ReactstrapPagination listClassName="justify-content-center my-4">
|
||||||
|
<PaginationItem>{firstButton}</PaginationItem>
|
||||||
|
<PaginationItem>{prevButton}</PaginationItem>
|
||||||
|
<PaginationItem>{prevChunk}</PaginationItem>
|
||||||
|
{pageButtons}
|
||||||
|
<PaginationItem>{nextChunk}</PaginationItem>
|
||||||
|
<PaginationItem>{nextButton}</PaginationItem>
|
||||||
|
<PaginationItem>{lastButton}</PaginationItem>
|
||||||
|
</ReactstrapPagination>
|
||||||
|
</nav>
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
SSRPagination
|
||||||
|
}
|
||||||
42
clientapp/src/components/Pagination/utils.ts
Normal file
42
clientapp/src/components/Pagination/utils.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
const intToArray = (value: number) => {
|
||||||
|
const array = []
|
||||||
|
for (let i = 1; i <= value; i++) {
|
||||||
|
array.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitInChunks = (array: number[], chunkSize: number) => {
|
||||||
|
const chunks = []
|
||||||
|
for (let i = 0, j = array.length; i < j; i += chunkSize) {
|
||||||
|
const temparray = array.slice(i, i + chunkSize)
|
||||||
|
chunks.push(temparray)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
const findChunk = (chunks: number[][], page: number) => {
|
||||||
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
|
for (let j = 0; j < chunks[i].length; j++) {
|
||||||
|
if (chunks[i][j] === page) {
|
||||||
|
return {
|
||||||
|
index: i,
|
||||||
|
items: chunks[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
index: 0,
|
||||||
|
items: chunks[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
intToArray,
|
||||||
|
splitInChunks,
|
||||||
|
findChunk
|
||||||
|
}
|
||||||
@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import { IBlogItemModel, ICategoryModel } from "../models"
|
|
||||||
|
|
||||||
const apiUrl = 'https://localhost:59018/api/Blog'
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
43
clientapp/src/functions/findRoutes.ts
Normal file
43
clientapp/src/functions/findRoutes.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { RouteModel } from "../models"
|
||||||
|
|
||||||
|
interface ComponentRoutesModel {
|
||||||
|
targets: string [],
|
||||||
|
component: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const findRoutes = (routes: RouteModel[] = [], component: string | undefined, parentTarget: string [] = [], result: ComponentRoutesModel [] = []): ComponentRoutesModel [] => {
|
||||||
|
|
||||||
|
if(!Array.isArray(routes))
|
||||||
|
return []
|
||||||
|
|
||||||
|
routes.forEach((route: RouteModel) => {
|
||||||
|
const targets: string [] = []
|
||||||
|
if(parentTarget) {
|
||||||
|
parentTarget.forEach(item => {
|
||||||
|
targets.push(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
targets.push(route.target)
|
||||||
|
|
||||||
|
if(route.component) {
|
||||||
|
result.push({
|
||||||
|
targets,
|
||||||
|
component: route.component
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Array.isArray(route.childRoutes)) {
|
||||||
|
findRoutes(route.childRoutes, component, targets, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if(component) {
|
||||||
|
result = result.filter(x => x.component === component)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
findRoutes
|
||||||
|
}
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import { dateFormat } from './dateFormat'
|
import { dateFormat } from './dateFormat'
|
||||||
|
import { findRoutes } from './findRoutes'
|
||||||
import { getKeyValue } from './getKeyValue'
|
import { getKeyValue } from './getKeyValue'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getKeyValue,
|
getKeyValue,
|
||||||
dateFormat
|
dateFormat,
|
||||||
|
findRoutes
|
||||||
}
|
}
|
||||||
@ -1,12 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { Container } from 'reactstrap'
|
import { Container } from 'reactstrap'
|
||||||
|
import { ApplicationState } from '../../../store'
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
// let { siteName } = useSelector((state: IReduxState) => state.settings)
|
const content = useSelector((state: ApplicationState) => state.content)
|
||||||
|
|
||||||
return <footer className="py-3 bg-dark">
|
return <footer className="py-3 bg-dark">
|
||||||
{/*<Container fluid><p className="m-0 text-center text-white">Copyright © {siteName} {(new Date).getFullYear()}</p></Container>*/}
|
<Container fluid><p className="m-0 text-center text-white">Copyright © {content?.siteName} {(new Date).getFullYear()}</p></Container>
|
||||||
</footer>
|
</footer>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,13 +3,12 @@ import { Link } from 'react-router-dom'
|
|||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { Collapse, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'
|
import { Collapse, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'
|
||||||
import { FeatherIcon } from '../../../components/FeatherIcons'
|
import { FeatherIcon } from '../../../components/FeatherIcons'
|
||||||
|
import { ApplicationState } from '../../../store'
|
||||||
|
import { MenuItemModel } from '../../../models'
|
||||||
|
|
||||||
|
|
||||||
const NavMenu : FC = () => {
|
const NavMenu : FC = () => {
|
||||||
/*
|
const content = useSelector((state: ApplicationState) => state.content)
|
||||||
let { siteName, topMenu = [] } = useSelector((state: IReduxState) => {
|
|
||||||
return state.settings
|
|
||||||
})
|
|
||||||
|
|
||||||
const [state, hookState] = useState({
|
const [state, hookState] = useState({
|
||||||
isOpen: false
|
isOpen: false
|
||||||
@ -20,23 +19,21 @@ const NavMenu : FC = () => {
|
|||||||
isOpen: !state.isOpen
|
isOpen: !state.isOpen
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
return <header>
|
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">
|
||||||
<NavbarBrand href="/">{siteName}</ NavbarBrand>
|
<NavbarBrand href="/">{content?.siteName}</ NavbarBrand>
|
||||||
<NavbarToggler onClick={toggle} className="mr-2"/>
|
<NavbarToggler onClick={toggle} className="mr-2"/>
|
||||||
<Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={state.isOpen} navbar>
|
<Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={state.isOpen} navbar>
|
||||||
<ul className="navbar-nav flex-grow">
|
<ul className="navbar-nav flex-grow">
|
||||||
{topMenu.map((item: IMenuItemModel, index: number) => {
|
{content?.topMenu ? content.topMenu.map((item: MenuItemModel, index: number) => {
|
||||||
return <NavItem key={index}>
|
return <NavItem key={index}>
|
||||||
<NavLink tag={Link} className="text-dark" to={item.target}>
|
<NavLink tag={Link} className="text-dark" to={item.target}>
|
||||||
{item.icon ? <FeatherIcon icon={item.icon}/> : ''}
|
{item.icon ? <FeatherIcon icon={item.icon}/> : ''}
|
||||||
{item.title}
|
{item.title}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
})}
|
}) : ''}
|
||||||
</ul>
|
</ul>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|
||||||
@ -47,7 +44,6 @@ const NavMenu : FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
*/}
|
|
||||||
</header>
|
</header>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
clientapp/src/models/abstractions.ts
Normal file
37
clientapp/src/models/abstractions.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { AuthorModel, ImageModel } from "./"
|
||||||
|
|
||||||
|
|
||||||
|
export interface RequestModel {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseModel {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageSectionModel {
|
||||||
|
title?: string
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersonModel {
|
||||||
|
id: string,
|
||||||
|
image: ImageModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostItemModel {
|
||||||
|
id: string,
|
||||||
|
slug: string,
|
||||||
|
image: ImageModel,
|
||||||
|
badge: string,
|
||||||
|
title: string,
|
||||||
|
shortText: string,
|
||||||
|
text: string,
|
||||||
|
author: AuthorModel,
|
||||||
|
created: string,
|
||||||
|
tags: string []
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageModel {
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,35 +1,49 @@
|
|||||||
export interface IImageModel {
|
import { PageSectionModel, PersonModel, PostItemModel } from "./abstractions"
|
||||||
src: string,
|
|
||||||
alt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAuthorModel {
|
export interface AuthorModel extends PersonModel {
|
||||||
id: string,
|
|
||||||
image?: IImageModel,
|
|
||||||
nickName: string
|
nickName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BlogItemModel extends PostItemModel {
|
||||||
interface IPostItemModel {
|
|
||||||
id: string,
|
|
||||||
slug: string,
|
|
||||||
badge?: string,
|
|
||||||
image?: IImageModel,
|
|
||||||
title: string,
|
|
||||||
shortText: string,
|
|
||||||
text: string,
|
|
||||||
author: IAuthorModel,
|
|
||||||
created: string,
|
|
||||||
tags: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IBlogItemModel extends IPostItemModel {
|
|
||||||
readTime?: number,
|
readTime?: number,
|
||||||
likes?: number
|
likes?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IShopItemModel extends IPostItemModel {
|
export interface CategoryModel {
|
||||||
images?: IImageModel [],
|
id: string,
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeatureModel {
|
||||||
|
icon: string,
|
||||||
|
title: string,
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageModel {
|
||||||
|
src: string,
|
||||||
|
alt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuItemModel {
|
||||||
|
icon?: string,
|
||||||
|
title?: string,
|
||||||
|
target?: string
|
||||||
|
childItems?: MenuItemModel []
|
||||||
|
}
|
||||||
|
export interface ReviewerModel extends PersonModel {
|
||||||
|
fullName: string,
|
||||||
|
position: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteModel {
|
||||||
|
target: string
|
||||||
|
component?: string
|
||||||
|
childRoutes?: RouteModel []
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShopItemModel extends PostItemModel {
|
||||||
|
images?: ImageModel [],
|
||||||
sku: string,
|
sku: string,
|
||||||
rating?: number,
|
rating?: number,
|
||||||
price: number,
|
price: number,
|
||||||
@ -37,38 +51,19 @@ export interface IShopItemModel extends IPostItemModel {
|
|||||||
quantity?: number
|
quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TestimonialsModel {
|
||||||
|
text: string,
|
||||||
|
reviewer: ReviewerModel
|
||||||
|
}
|
||||||
|
|
||||||
interface IPostPaginationModel {
|
export interface PaginationModel<T> {
|
||||||
|
totalPages: number,
|
||||||
currentPage: number,
|
currentPage: number,
|
||||||
totalPages: number
|
items: T []
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBlogItemsPaginationModel extends IPostPaginationModel {
|
export interface FormItemModel {
|
||||||
items: IBlogItemModel []
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IShopItemsPaginationModel extends IPostPaginationModel {
|
|
||||||
items: IShopItemModel []
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICategoryModel {
|
|
||||||
id: string,
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRouteModel {
|
|
||||||
target: string
|
|
||||||
component?: string
|
|
||||||
childRoutes?: IRouteModel []
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMenuItemModel {
|
|
||||||
icon?: string,
|
|
||||||
title?: string,
|
title?: string,
|
||||||
target?: string
|
placeHolder?: string
|
||||||
childItems?: IMenuItemModel []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPageModel {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
25
clientapp/src/models/pageSections.ts
Normal file
25
clientapp/src/models/pageSections.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { BlogItemModel, FeatureModel, FormItemModel, ImageModel, MenuItemModel, TestimonialsModel } from "./"
|
||||||
|
import { PageSectionModel } from "./abstractions"
|
||||||
|
|
||||||
|
export interface CallToActionSectionModel extends PageSectionModel {
|
||||||
|
privacyDisclaimer: string
|
||||||
|
email: FormItemModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeaturedBlogsSectionModel extends PageSectionModel {
|
||||||
|
items: BlogItemModel []
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeaturesSectionModel extends PageSectionModel {
|
||||||
|
items: FeatureModel []
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestimonialsSectionModel extends PageSectionModel {
|
||||||
|
items: TestimonialsModel []
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TitleSectionModel extends PageSectionModel {
|
||||||
|
image?: ImageModel,
|
||||||
|
primaryLink?: MenuItemModel,
|
||||||
|
secondaryLink?: MenuItemModel
|
||||||
|
}
|
||||||
18
clientapp/src/models/pages.ts
Normal file
18
clientapp/src/models/pages.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { PageModel } from "./abstractions"
|
||||||
|
import { CallToActionSectionModel, FeaturedBlogsSectionModel, FeaturesSectionModel, TestimonialsSectionModel, TitleSectionModel } from "./pageSections"
|
||||||
|
|
||||||
|
export interface HomePageModel extends PageModel {
|
||||||
|
titleSection: TitleSectionModel,
|
||||||
|
featuresSection: FeaturesSectionModel,
|
||||||
|
testimonialsSection: TestimonialsSectionModel,
|
||||||
|
featuredBlogsSection: FeaturedBlogsSectionModel,
|
||||||
|
callToActionSection: CallToActionSectionModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShopCatalogPageModel extends PageModel {
|
||||||
|
titleSection: TitleSectionModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogCatalogPageModel extends PageModel {
|
||||||
|
titleSection: TitleSectionModel
|
||||||
|
}
|
||||||
22
clientapp/src/models/requests.ts
Normal file
22
clientapp/src/models/requests.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface GetShopCatalogRequestModel {
|
||||||
|
[key: string]: string | undefined
|
||||||
|
category?: string,
|
||||||
|
searchText?: string,
|
||||||
|
currentPage?: string,
|
||||||
|
itemsPerPage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetBlogCatalogRequestModel {
|
||||||
|
[key: string]: string | undefined
|
||||||
|
category?: string,
|
||||||
|
searchText?: string,
|
||||||
|
currentPage?: string,
|
||||||
|
itemsPerPage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetStaticContentRequestModel {
|
||||||
|
[key: string]: string | undefined
|
||||||
|
locale?: string
|
||||||
|
}
|
||||||
35
clientapp/src/models/responses.ts
Normal file
35
clientapp/src/models/responses.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { BlogItemModel, CategoryModel, MenuItemModel, PaginationModel, RouteModel, ShopItemModel } from "./"
|
||||||
|
import { ResponseModel } from "./abstractions"
|
||||||
|
import { BlogCatalogPageModel, HomePageModel, ShopCatalogPageModel } from "./pages"
|
||||||
|
|
||||||
|
export interface GetBlogCatalogResponseModel extends ResponseModel {
|
||||||
|
featuredBlog: BlogItemModel,
|
||||||
|
categories: CategoryModel [],
|
||||||
|
blogItemsPagination: PaginationModel<BlogItemModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetShopCatalogResponseModel extends ResponseModel {
|
||||||
|
shopItemsPagination: PaginationModel<ShopItemModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetStaticContentResponseModel extends ResponseModel {
|
||||||
|
siteName: string,
|
||||||
|
|
||||||
|
routes: RouteModel [],
|
||||||
|
adminRoutes: RouteModel [],
|
||||||
|
serviceRoutes: RouteModel [],
|
||||||
|
|
||||||
|
topMenu: MenuItemModel [],
|
||||||
|
sideMenu: MenuItemModel [],
|
||||||
|
|
||||||
|
homePage: HomePageModel,
|
||||||
|
shopCatalog: ShopCatalogPageModel,
|
||||||
|
blogCatalog: BlogCatalogPageModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetWeatherForecastResponseModel extends ResponseModel {
|
||||||
|
date: string,
|
||||||
|
temperatireC: number,
|
||||||
|
temperatureF: number,
|
||||||
|
summary?: string
|
||||||
|
}
|
||||||
@ -1,24 +1,40 @@
|
|||||||
import React, { FC, useEffect, useState } from 'react'
|
import React, { FC, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { Card, CardBody, CardFooter, CardHeader, CardImg, Col, Container, Row } from 'reactstrap'
|
|
||||||
import { dateFormat } from '../../../functions'
|
|
||||||
|
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import { actionCreators as loaderActionCreators } from '../../../store/reducers/Loader'
|
||||||
|
import { actionCreators as blogCatalogActionCreators } from '../../../store/reducers/BlogCatalog'
|
||||||
|
|
||||||
import { GetBlogCatalog, IGetBlogCatalogResponse } from '../../../controllers/blogCatalog'
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||||
import { IBlogItemModel, IBlogItemsPaginationModel } from '../../../models'
|
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
|
||||||
|
|
||||||
|
import { dateFormat, findRoutes } from '../../../functions'
|
||||||
|
import { BlogItemModel, PaginationModel } from '../../../models'
|
||||||
|
import { ApplicationState } from '../../../store'
|
||||||
|
|
||||||
import { Categories, Empty, Search } from '../SideWidgets'
|
import { Categories, Empty, Search } from '../SideWidgets'
|
||||||
|
import { TitleSectionModel } from '../../../models/pageSections'
|
||||||
|
import { Pagination } from '../../../components/Pagination'
|
||||||
|
|
||||||
|
const TitleSection: FC<TitleSectionModel> = (props) => {
|
||||||
|
const { title, text } = props
|
||||||
|
return <header className="py-5 bg-light border-bottom mb-4">
|
||||||
|
<Container fluid>
|
||||||
|
<div className="text-center my-5">
|
||||||
|
<h1 className="fw-bolder">{title ? title : ''}</h1>
|
||||||
|
<p className="lead mb-0">{text ? text : ''}</p>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</header>
|
||||||
|
}
|
||||||
|
|
||||||
const FeaturedBlog: FC<IBlogItemModel> = (props) => {
|
const FeaturedBlog: FC<BlogItemModel> = (props) => {
|
||||||
const { id, slug, badge, image, title, shortText, author, created, readTime, likes, tags } = props
|
const { id, slug, badge, image, title, shortText, author, created, readTime, likes, tags } = props
|
||||||
|
|
||||||
return <Card className="mb-4 shadow border-0">
|
return <Card className="mb-4 shadow border-0">
|
||||||
<CardImg top {...image} />
|
<CardImg top {...image} />
|
||||||
<CardBody className="p-4">
|
<CardBody className="p-4">
|
||||||
<div className="badge bg-primary bg-gradient rounded-pill mb-2">{badge}</div>
|
<div className="badge bg-primary bg-gradient rounded-pill mb-2">{badge}</div>
|
||||||
<Link className="text-decoration-none link-dark stretched-link" to={`blog/item/${slug}`}>
|
<Link className="text-decoration-none link-dark stretched-link" to={`/blog/${slug}`}>
|
||||||
<h5 className="card-title mb-3">{title}</h5>
|
<h5 className="card-title mb-3">{title}</h5>
|
||||||
</Link>
|
</Link>
|
||||||
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: shortText }}></p>
|
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: shortText }}></p>
|
||||||
@ -38,8 +54,15 @@ const FeaturedBlog: FC<IBlogItemModel> = (props) => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BlogItemsPagination: FC<IBlogItemsPaginationModel> = (props) => {
|
interface BlogItemsPaginationModel extends PaginationModel<BlogItemModel> {
|
||||||
const { items, currentPage, totalPages } = props
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlogItemsPagination: FC<BlogItemsPaginationModel> = (props) => {
|
||||||
|
const { items, currentPage, totalPages, path } = props
|
||||||
|
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{items.map((item, index) => <Col key={index} className="lg-6">
|
{items.map((item, index) => <Col key={index} className="lg-6">
|
||||||
@ -48,61 +71,66 @@ const BlogItemsPagination: FC<IBlogItemsPaginationModel> = (props) => {
|
|||||||
<CardImg top {...item.image} />
|
<CardImg top {...item.image} />
|
||||||
|
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<div className="small text-muted">{item.created}</div>
|
<div className="small text-muted">{dateFormat(item.created)}</div>
|
||||||
<h2 className="card-title h4">{item.title}</h2>
|
<h2 className="card-title h4">{item.title}</h2>
|
||||||
<p className="card-text">{item.shortText}</p>
|
<p className="card-text">{item.shortText}</p>
|
||||||
<Link to={`${currentPage}/${item.slug}`} className="btn btn-primary">Read more →</Link>
|
<Link to={`${path}/${currentPage}/${item.slug}`} className="btn btn-primary">Read more →</Link>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
</Col>)}
|
</Col>)}
|
||||||
|
|
||||||
<nav aria-label="Pagination">
|
<Pagination {...{
|
||||||
<hr className="my-0" />
|
totalPages: totalPages,
|
||||||
<ul className="pagination justify-content-center my-4">
|
currentPage: currentPage,
|
||||||
<li className="page-item disabled"><a className="page-link" href="#" aria-disabled="true">Newer</a></li>
|
onClick: (nextPage) => {
|
||||||
<li className="page-item active" aria-current="page"><a className="page-link" href="#!">1</a></li>
|
dispatch(blogCatalogActionCreators.requestBlogCatalog({
|
||||||
<li className="page-item"><a className="page-link" href="#!">2</a></li>
|
currentPage: nextPage + ""
|
||||||
<li className="page-item"><a className="page-link" href="#!">3</a></li>
|
}))
|
||||||
<li className="page-item disabled"><a className="page-link" href="#!">...</a></li>
|
|
||||||
<li className="page-item"><a className="page-link" href="#!">15</a></li>
|
navigate(`${path}/${nextPage}`)
|
||||||
<li className="page-item"><a className="page-link" href="#!">Older</a></li>
|
}
|
||||||
</ul>
|
}} />
|
||||||
</nav>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
const BlogCatalog = () => {
|
const BlogCatalog = () => {
|
||||||
const [state, setState] = useState<IGetBlogCatalogResponse>()
|
const params = useParams()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const content = useSelector((state: ApplicationState) => state.content)
|
||||||
|
const page = content?.blogCatalog
|
||||||
|
const path = findRoutes(content?.routes, 'BlogCatalog')[0]?.targets[0]
|
||||||
|
|
||||||
|
const blogCatalog = useSelector((state: ApplicationState) => state.blogCatalog)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
GetBlogCatalog().then(response => {
|
dispatch(blogCatalogActionCreators.requestBlogCatalog({
|
||||||
setState(response)
|
currentPage: params?.page ? params.page : "1"
|
||||||
})
|
}))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <>
|
useEffect(() => {
|
||||||
<header className="py-5 bg-light border-bottom mb-4">
|
blogCatalog?.isLoading
|
||||||
<Container fluid>
|
? dispatch(loaderActionCreators.show())
|
||||||
<div className="text-center my-5">
|
: setTimeout(() => {
|
||||||
<h1 className="fw-bolder">Welcome to Blog Home!</h1>
|
dispatch(loaderActionCreators.hide())
|
||||||
<p className="lead mb-0">A Bootstrap 5 starter layout for your next blog homepage</p>
|
}, 1000)
|
||||||
</div>
|
}, [blogCatalog?.isLoading])
|
||||||
</Container>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<TitleSection {...page?.titleSection} />
|
||||||
<Container fluid>
|
<Container fluid>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
{state?.featuredBlog ? <FeaturedBlog {...state.featuredBlog} /> : ''}
|
{blogCatalog?.featuredBlog ? <FeaturedBlog {...blogCatalog.featuredBlog} /> : ''}
|
||||||
<Row>
|
<Row>
|
||||||
{state?.blogItemsPagination ? <BlogItemsPagination {...state.blogItemsPagination} /> : '' }
|
{blogCatalog?.blogItemsPagination ? <BlogItemsPagination path={path} {...blogCatalog.blogItemsPagination} /> : '' }
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg="4">
|
<Col lg="4">
|
||||||
<Search />
|
<Search />
|
||||||
{state?.categories ? <Categories {...{
|
{blogCatalog?.categories ? <Categories {...{
|
||||||
categories: state.categories
|
categories: blogCatalog.categories
|
||||||
}} /> : '' }
|
}} /> : '' }
|
||||||
<Empty/>
|
<Empty/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Card, CardBody, CardHeader, Col, Row } from 'reactstrap'
|
import { Card, CardBody, CardHeader, Col, Row } from 'reactstrap'
|
||||||
import { ICategoryModel } from '../../../models'
|
import { CategoryModel } from '../../../models'
|
||||||
|
|
||||||
const Search = () => {
|
const Search = () => {
|
||||||
return <Card className="mb-4">
|
return <Card className="mb-4">
|
||||||
@ -14,8 +14,8 @@ const Search = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICategories {
|
interface ICategories {
|
||||||
categories?: ICategoryModel []
|
categories?: CategoryModel []
|
||||||
}
|
}
|
||||||
|
|
||||||
const Categories = (props: ICategories) => {
|
const Categories = (props: ICategories) => {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Link, useLocation, useParams } from 'react-router-dom'
|
|||||||
// Redux
|
// Redux
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
import { actionCreators as weatherForecastsActionCreators, WeatherForecast, WeatherForecastsState } from '../store/reducers/WeatherForecasts'
|
import { actionCreators as weatherForecastsActionCreators, WeatherForecast, WeatherForecastsState } from '../store/reducers/WeatherForecasts'
|
||||||
|
import { dateFormat } from '../functions'
|
||||||
|
|
||||||
interface IReduxState {
|
interface IReduxState {
|
||||||
weatherForecasts: WeatherForecastsState
|
weatherForecasts: WeatherForecastsState
|
||||||
@ -45,7 +46,7 @@ const FetchData = () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{forecasts.map((forecast: WeatherForecast) =>
|
{forecasts.map((forecast: WeatherForecast) =>
|
||||||
<tr key={forecast.date}>
|
<tr key={forecast.date}>
|
||||||
<td>{forecast.date}</td>
|
<td>{dateFormat(forecast.date)}</td>
|
||||||
<td>{forecast.temperatureC}</td>
|
<td>{forecast.temperatureC}</td>
|
||||||
<td>{forecast.temperatureF}</td>
|
<td>{forecast.temperatureF}</td>
|
||||||
<td>{forecast.summary}</td>
|
<td>{forecast.summary}</td>
|
||||||
|
|||||||
@ -1,56 +1,27 @@
|
|||||||
import React, { FC } from 'react'
|
// React
|
||||||
import { useSelector } from 'react-redux'
|
import React, { FC, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
|
|
||||||
import { FeatherIcon } from '../../components/FeatherIcons'
|
// Redux
|
||||||
import { IPageModel, IBlogItemModel, IImageModel } from '../../models'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { ApplicationState } from '../../store'
|
import { ApplicationState } from '../../store'
|
||||||
import { IContentState } from '../../store/reducers/Content'
|
import { actionCreators as loaderActionCreators } from '../../store/reducers/Loader'
|
||||||
|
|
||||||
|
// Reactstrap
|
||||||
|
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
|
||||||
|
|
||||||
|
// Models (interfaces)
|
||||||
|
import { CallToActionSectionModel, FeaturedBlogsSectionModel, FeaturesSectionModel, TestimonialsSectionModel, TitleSectionModel } from '../../models/pageSections'
|
||||||
|
|
||||||
|
// Custom components
|
||||||
|
import { FeatherIcon } from '../../components/FeatherIcons'
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
import { dateFormat } from '../../functions'
|
||||||
|
|
||||||
import style from './scss/style.module.scss'
|
import style from './scss/style.module.scss'
|
||||||
|
|
||||||
interface ITitleSection {
|
const TitleSection : FC<TitleSectionModel> = (props) => {
|
||||||
title: string,
|
|
||||||
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
|
const { title, text } = props
|
||||||
|
|
||||||
return <header className="py-5 bg-dark">
|
return <header className="py-5 bg-dark">
|
||||||
@ -59,7 +30,7 @@ const TitleSection : FC<ITitleSection> = (props) => {
|
|||||||
<Col className="lg-8 xl-7 xxl-6">
|
<Col className="lg-8 xl-7 xxl-6">
|
||||||
<div className="my-5 text-center text-xl-start">
|
<div className="my-5 text-center text-xl-start">
|
||||||
<h1 className="display-5 fw-bolder text-white mb-2">{title}</h1>
|
<h1 className="display-5 fw-bolder text-white mb-2">{title}</h1>
|
||||||
<span className="lead fw-normal text-white-50 mb-4" dangerouslySetInnerHTML={{ __html: text }}>
|
<span className="lead fw-normal text-white-50 mb-4" dangerouslySetInnerHTML={{ __html: text ? text : "" }}>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
<div className="d-grid gap-3 d-sm-flex justify-content-sm-center justify-content-xl-start">
|
<div className="d-grid gap-3 d-sm-flex justify-content-sm-center justify-content-xl-start">
|
||||||
@ -77,46 +48,45 @@ const TitleSection : FC<ITitleSection> = (props) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const FeaturesSection: FC<IFeaturesSection> = (props) => {
|
const FeaturesSection: FC<FeaturesSectionModel> = (props) => {
|
||||||
const { title, items } = props
|
const { title, items } = props
|
||||||
|
|
||||||
return <section className="py-5" id="features">
|
return <section className="py-5" id="features">
|
||||||
<Container fluid className="px-5 my-5">
|
<Container fluid className="px-5 my-5">
|
||||||
<Row className="gx-5">
|
<Row className="gx-5">
|
||||||
<Col className="lg-4 mb-5 mb-lg-0">
|
<Col className="lg-4 mb-5 mb-lg-0">
|
||||||
<h2 className="fw-bolder mb-0">{title}</h2>
|
<h2 className="fw-bolder mb-0">{title ? title : ''}</h2>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="lg-8">
|
<Col className="lg-8">
|
||||||
<Row className="gx-5 cols-1 cols-md-2">
|
<Row className="gx-5 cols-1 cols-md-2">
|
||||||
{items.map((item, index) => <Col key={index} className="mb-5 h-100">
|
{items ? items.map((item, index) => <Col key={index} className="mb-5 h-100">
|
||||||
<div className={`${style.feature} bg-primary bg-gradient text-white rounded-3 mb-3`}>
|
<div className={`${style.feature} bg-primary bg-gradient text-white rounded-3 mb-3`}>
|
||||||
<FeatherIcon icon={item.icon} />
|
<FeatherIcon icon={item.icon} />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="h5">{item.title}</h2>
|
<h2 className="h5">{item.title}</h2>
|
||||||
<p className="mb-0" dangerouslySetInnerHTML={{ __html: item.text }}></p>
|
<p className="mb-0" dangerouslySetInnerHTML={{ __html: item.text }}></p>
|
||||||
</Col>)}
|
</Col>) : ''}
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TestimonialsSection: FC<TestimonialsSectionModel> = (props) => {
|
||||||
|
const item = props?.items ? props?.items.shift() : undefined
|
||||||
|
|
||||||
|
if(!item) return <></>
|
||||||
const TestimonialsSection: FC<ITestimonialsSection> = (props) => {
|
|
||||||
const { text, image } = props
|
|
||||||
|
|
||||||
return <section className="py-5 bg-light">
|
return <section className="py-5 bg-light">
|
||||||
<Container fluid className="px-5 my-5">
|
<Container fluid className="px-5 my-5">
|
||||||
<Row className="gx-5 justify-content-center">
|
<Row className="gx-5 justify-content-center">
|
||||||
<Col className="lg-10 xl-7">
|
<Col className="lg-10 xl-7">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="fs-4 mb-4 fst-italic" dangerouslySetInnerHTML={{ __html: text }}></div>
|
<div className="fs-4 mb-4 fst-italic" dangerouslySetInnerHTML={{ __html: item.text }}></div>
|
||||||
<div className="d-flex align-items-center justify-content-center">
|
<div className="d-flex align-items-center justify-content-center">
|
||||||
<img className="rounded-circle me-3" {...image} />
|
<img className="rounded-circle me-3" {...item.reviewer.image} />
|
||||||
<div className="fw-bold">Tom Ato<span className="fw-bold text-primary mx-1">/</span>CEO, Pomodoro
|
<div className="fw-bold">{item.reviewer.fullName}<span className="fw-bold text-primary mx-1">/</span>{item.reviewer.position}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -126,12 +96,7 @@ const TestimonialsSection: FC<ITestimonialsSection> = (props) => {
|
|||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FromOurBlogSection: FC<FeaturedBlogsSectionModel> = (props) => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const FromOurBlogSection: FC<IFeaturedBlogsSection> = (props) => {
|
|
||||||
const { title, text, items } = props
|
const { title, text, items } = props
|
||||||
|
|
||||||
return <section className="py-5">
|
return <section className="py-5">
|
||||||
@ -139,13 +104,13 @@ const FromOurBlogSection: FC<IFeaturedBlogsSection> = (props) => {
|
|||||||
<Row className="gx-5 justify-content-center">
|
<Row className="gx-5 justify-content-center">
|
||||||
<Col className="lg-8 xl-6">
|
<Col className="lg-8 xl-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="fw-bolder">{title}</h2>
|
<h2 className="fw-bolder">{title ? title : ''}</h2>
|
||||||
<p className="lead fw-normal text-muted mb-5" dangerouslySetInnerHTML={{ __html: text }}></p>
|
<p className="lead fw-normal text-muted mb-5" dangerouslySetInnerHTML={{ __html: text ? text : '' }}></p>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="gx-5">
|
<Row className="gx-5">
|
||||||
{items.map((item, index) => <Col key={index} className="lg-4 mb-5">
|
{items ? items.map((item, index) => <Col key={index} className="lg-4 mb-5">
|
||||||
<Card className="h-100 shadow border-0">
|
<Card className="h-100 shadow border-0">
|
||||||
<CardImg top {...item.image} />
|
<CardImg top {...item.image} />
|
||||||
<CardBody className="p-4">
|
<CardBody className="p-4">
|
||||||
@ -153,7 +118,7 @@ const FromOurBlogSection: FC<IFeaturedBlogsSection> = (props) => {
|
|||||||
<Link className="text-decoration-none link-dark stretched-link" to="#!">
|
<Link className="text-decoration-none link-dark stretched-link" to="#!">
|
||||||
<h5 className="card-title mb-3">{item.title}</h5>
|
<h5 className="card-title mb-3">{item.title}</h5>
|
||||||
</Link>
|
</Link>
|
||||||
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: text }}></p>
|
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: text ? text : '' }}></p>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter className="p-4 pt-0 bg-transparent border-top-0">
|
<CardFooter className="p-4 pt-0 bg-transparent border-top-0">
|
||||||
<div className="d-flex align-items-end justify-content-between">
|
<div className="d-flex align-items-end justify-content-between">
|
||||||
@ -161,22 +126,21 @@ const FromOurBlogSection: FC<IFeaturedBlogsSection> = (props) => {
|
|||||||
<img className="rounded-circle me-3" {...item.author.image} />
|
<img className="rounded-circle me-3" {...item.author.image} />
|
||||||
<div className="small">
|
<div className="small">
|
||||||
<div className="fw-bold">{item.author.nickName}</div>
|
<div className="fw-bold">{item.author.nickName}</div>
|
||||||
<div className="text-muted">{item.created} · {item.readTime}</div>
|
<div className="text-muted">{dateFormat(item.created)} · {item.readTime}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>)}
|
</Col>) : ''}
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CallToActionSection: FC<CallToActionSectionModel> = (props) => {
|
||||||
|
const { title, text, privacyDisclaimer, email } = props
|
||||||
|
|
||||||
|
|
||||||
const CallToActionSection: FC<ICallToActionSection> = (props) => {
|
|
||||||
const { title, text, privacyDisclaimer } = props
|
|
||||||
return <section className="py-5">
|
return <section className="py-5">
|
||||||
<Container fluid className="px-5 my-5">
|
<Container fluid className="px-5 my-5">
|
||||||
<aside className="bg-primary bg-gradient rounded-3 p-4 p-sm-5 mt-5">
|
<aside className="bg-primary bg-gradient rounded-3 p-4 p-sm-5 mt-5">
|
||||||
@ -187,8 +151,8 @@ const CallToActionSection: FC<ICallToActionSection> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="ms-xl-4">
|
<div className="ms-xl-4">
|
||||||
<div className="input-group mb-2">
|
<div className="input-group mb-2">
|
||||||
<input className="form-control" type="text" placeholder="Email address..." aria-label="Email address..." aria-describedby="button-newsletter" />
|
<input className="form-control" type="text" placeholder={email.placeHolder ? email.placeHolder : ''} aria-label={email.placeHolder ? email.placeHolder : ''} aria-describedby="button-newsletter" />
|
||||||
<button className="btn btn-outline-light" id="button-newsletter" type="button">Sign up</button>
|
<button className="btn btn-outline-light" id="button-newsletter" type="button">{email.title ? email.title : ''}</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="small text-white-50">{privacyDisclaimer}</div>
|
<div className="small text-white-50">{privacyDisclaimer}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -199,16 +163,27 @@ const CallToActionSection: FC<ICallToActionSection> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const state = useSelector((state: ApplicationState) => state.content)
|
const dispatch = useDispatch()
|
||||||
|
const content = useSelector((state: ApplicationState) => state.content)
|
||||||
|
|
||||||
const page = state?.pages?.filter(x => x.id == "HomePage").shift() as IHomePage
|
useEffect(() => {
|
||||||
|
content?.isLoading
|
||||||
|
? dispatch(loaderActionCreators.show())
|
||||||
|
: setTimeout(() => {
|
||||||
|
dispatch(loaderActionCreators.hide())
|
||||||
|
}, 1000)
|
||||||
|
}, [content?.isLoading])
|
||||||
|
|
||||||
|
const page = content?.homePage
|
||||||
|
|
||||||
|
if(!page) return <></>
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{ page?.titleSection ? <TitleSection {...page.titleSection}/> : '' }
|
<TitleSection {...page.titleSection} />
|
||||||
{ page?.featuresSection ? <FeaturesSection {...page.featuresSection} /> : '' }
|
<FeaturesSection {...page.featuresSection} />
|
||||||
{ page?.testimonialsSection ? <TestimonialsSection {...page.testimonialsSection} /> : '' }
|
<TestimonialsSection {...page.testimonialsSection} />
|
||||||
{ page?.featuredBlogsSection ? <FromOurBlogSection {...page.featuredBlogsSection} /> : '' }
|
<FromOurBlogSection {...page.featuredBlogsSection} />
|
||||||
{ page?.callToActionSection ? <CallToActionSection {...page.callToActionSection} /> :'' }
|
<CallToActionSection {...page.callToActionSection} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,50 @@
|
|||||||
import React, { FC, useEffect, useState } from 'react'
|
// React
|
||||||
import { Link } from 'react-router-dom'
|
import React, { FC, useEffect } from 'react'
|
||||||
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
|
// Redux
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { ApplicationState } from '../../../store'
|
||||||
|
import { actionCreators as loaderActionCreators } from '../../../store/reducers/Loader'
|
||||||
|
import { actionCreators as shopCatalogActionCreators } from '../../../store/reducers/ShopCatalog'
|
||||||
|
|
||||||
|
// Reactstrap
|
||||||
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
|
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
|
||||||
|
|
||||||
|
// Models (interfaces)
|
||||||
|
import { PaginationModel, ShopItemModel } from '../../../models'
|
||||||
|
import { TitleSectionModel } from '../../../models/pageSections'
|
||||||
|
|
||||||
|
// Custom components
|
||||||
import { FeatherRating } from '../../../components/FeatherRating'
|
import { FeatherRating } from '../../../components/FeatherRating'
|
||||||
|
import { Pagination } from '../../../components/Pagination'
|
||||||
|
|
||||||
import { IShopItemsPaginationModel } from '../../../models'
|
// Functions
|
||||||
import { IGetShopCatalogResponse, GetShopCatalog } from '../../../controllers/shopCatalog'
|
import { findRoutes } from '../../../functions'
|
||||||
|
|
||||||
|
const TitleSection: FC<TitleSectionModel> = (props) => {
|
||||||
|
const { title, text } = props
|
||||||
|
|
||||||
const ShopItemsPagination: FC<IShopItemsPaginationModel> = (props) => {
|
return <header className="bg-dark py-5">
|
||||||
const { items, currentPage, totalPages } = props
|
<Container fluid className="px-4 px-lg-5 my-5">
|
||||||
|
|
||||||
|
<div className="text-center text-white">
|
||||||
|
<h1 className="display-4 fw-bolder">{title ? title : ''}</h1>
|
||||||
|
<p className="lead fw-normal text-white-50 mb-0">{text ? text : ''}</p>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</header>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShopItemsPaginationModel extends PaginationModel<ShopItemModel> {
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShopItemsPagination: FC<ShopItemsPaginationModel> = (props) => {
|
||||||
|
const { items, currentPage, totalPages, path } = props
|
||||||
|
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return <section className="py-5">
|
return <section className="py-5">
|
||||||
<Container fluid className="px-4 px-lg-5 mt-5">
|
<Container fluid className="px-4 px-lg-5 mt-5">
|
||||||
@ -18,7 +53,7 @@ const ShopItemsPagination: FC<IShopItemsPaginationModel> = (props) => {
|
|||||||
<Card className="h-100">
|
<Card className="h-100">
|
||||||
<div className="badge bg-dark text-white position-absolute" style={{top: "0.5rem", right: "0.5rem"}}>{item.badge}</div>
|
<div className="badge bg-dark text-white position-absolute" style={{top: "0.5rem", right: "0.5rem"}}>{item.badge}</div>
|
||||||
|
|
||||||
<Link to={`${currentPage}/${item.slug}`}>
|
<Link to={`${path}/${currentPage}/${item.slug}`}>
|
||||||
<CardImg top {...item.image} />
|
<CardImg top {...item.image} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -43,82 +78,52 @@ const ShopItemsPagination: FC<IShopItemsPaginationModel> = (props) => {
|
|||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
<Pagination {...{
|
||||||
|
totalPages: totalPages,
|
||||||
|
currentPage: currentPage,
|
||||||
|
onClick: (nextPage) => {
|
||||||
|
dispatch(shopCatalogActionCreators.requestShopCatalog({
|
||||||
|
currentPage: nextPage + ""
|
||||||
|
}))
|
||||||
|
|
||||||
|
navigate(`${path}/${nextPage}`)
|
||||||
|
}
|
||||||
|
}} />
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ShopCatalog = () => {
|
const ShopCatalog = () => {
|
||||||
|
const params = useParams()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
const items = [
|
const content = useSelector((state: ApplicationState) => state.content)
|
||||||
{
|
const page = content?.shopCatalog
|
||||||
id: "1",
|
const path = findRoutes(content?.routes, 'ShopCatalog')[0]?.targets[0]
|
||||||
rating: 5,
|
|
||||||
price: "$20.00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
rating: 3.5,
|
|
||||||
price: "$20.00",
|
|
||||||
newPrice: "$10.00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
rating: 2,
|
|
||||||
price: "$20.00",
|
|
||||||
newPrice: "$10.00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
rating: 4,
|
|
||||||
price: "$20.00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5",
|
|
||||||
rating: 4.5,
|
|
||||||
price: "$20.00",
|
|
||||||
newPrice: "$10.00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "6",
|
|
||||||
rating: 5,
|
|
||||||
price: "$20.00",
|
|
||||||
newPrice: "$10.00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "7",
|
|
||||||
rating: 2,
|
|
||||||
price: "$20.00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "8",
|
|
||||||
rating: 3,
|
|
||||||
price: "$20.00",
|
|
||||||
newPrice: "$10.00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const [state, setState] = useState<IGetShopCatalogResponse>()
|
|
||||||
|
|
||||||
|
const shopCatalog = useSelector((state: ApplicationState) => state.shopCatalog)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
GetShopCatalog().then(response => {
|
dispatch(shopCatalogActionCreators.requestShopCatalog({
|
||||||
setState(response)
|
currentPage: params?.page ? params.page : "1"
|
||||||
})
|
}))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
shopCatalog?.isLoading
|
||||||
|
? dispatch(loaderActionCreators.show())
|
||||||
|
: setTimeout(() => {
|
||||||
|
dispatch(loaderActionCreators.hide())
|
||||||
|
}, 1000)
|
||||||
|
}, [shopCatalog?.isLoading])
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<header className="bg-dark py-5">
|
<TitleSection {...page?.titleSection} />
|
||||||
<Container fluid className="px-4 px-lg-5 my-5">
|
{shopCatalog?.shopItemsPagination ? <ShopItemsPagination path={path} {...shopCatalog.shopItemsPagination} /> : ''}
|
||||||
|
|
||||||
<div className="text-center text-white">
|
|
||||||
<h1 className="display-4 fw-bolder">Shop in style</h1>
|
|
||||||
<p className="lead fw-normal text-white-50 mb-0">With this shop hompeage template</p>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{state?.shopItemsPagination ? <ShopItemsPagination {...state.shopItemsPagination} /> : ''}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const Post = () => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Get = async <TResponse>(apiUrl: string, props?: IRequest): Promise<TResponse> => {
|
const Get = async <T>(apiUrl: string, props?: IRequest): Promise<T | null> => {
|
||||||
const url = new URL(apiUrl)
|
const url = new URL(apiUrl)
|
||||||
|
|
||||||
if(props) {
|
if(props) {
|
||||||
@ -40,7 +40,10 @@ const Get = async <TResponse>(apiUrl: string, props?: IRequest): Promise<TRespon
|
|||||||
console.log(err)
|
console.log(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
return JSON.parse((fetchData as IFetchResult).text) as TResponse
|
if (fetchData?.text)
|
||||||
|
return JSON.parse((fetchData as IFetchResult).text) as T
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const Put = () => {
|
const Put = () => {
|
||||||
|
|||||||
@ -1,12 +1,23 @@
|
|||||||
import * as WeatherForecasts from './reducers/WeatherForecasts'
|
import * as WeatherForecasts from './reducers/WeatherForecasts'
|
||||||
import * as Counter from './reducers/Counter'
|
import * as Counter from './reducers/Counter'
|
||||||
|
|
||||||
|
import * as Loader from './reducers/Loader'
|
||||||
|
|
||||||
import * as Content from './reducers/Content'
|
import * as Content from './reducers/Content'
|
||||||
|
import * as BlogCatalog from './reducers/BlogCatalog'
|
||||||
|
import * as ShopCatalog from './reducers/ShopCatalog'
|
||||||
|
|
||||||
|
|
||||||
// The top-level state object
|
// The top-level state object
|
||||||
export interface ApplicationState {
|
export interface ApplicationState {
|
||||||
counter: Counter.CounterState | undefined
|
counter: Counter.CounterState | undefined
|
||||||
weatherForecasts: WeatherForecasts.WeatherForecastsState | undefined
|
weatherForecasts: WeatherForecasts.WeatherForecastsState | undefined
|
||||||
content: Content.IContentState | undefined
|
|
||||||
|
loader: Loader.LoaderState | undefined
|
||||||
|
|
||||||
|
content: Content.ContentState | undefined
|
||||||
|
blogCatalog: BlogCatalog.BlogCatalogState | undefined
|
||||||
|
shopCatalog: ShopCatalog.ShopCatalogState | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whenever an action is dispatched, Redux will update each top-level application state property using
|
// Whenever an action is dispatched, Redux will update each top-level application state property using
|
||||||
@ -15,7 +26,12 @@ export interface ApplicationState {
|
|||||||
export const reducers = {
|
export const reducers = {
|
||||||
counter: Counter.reducer,
|
counter: Counter.reducer,
|
||||||
weatherForecasts: WeatherForecasts.reducer,
|
weatherForecasts: WeatherForecasts.reducer,
|
||||||
content: Content.reducer
|
|
||||||
|
loader: Loader.reducer,
|
||||||
|
|
||||||
|
content: Content.reducer,
|
||||||
|
blogCatalog: BlogCatalog.reducer,
|
||||||
|
shopCatalog: ShopCatalog.reducer
|
||||||
}
|
}
|
||||||
|
|
||||||
// This type can be used as a hint on action creators so that its 'dispatch' and 'getState' params are
|
// This type can be used as a hint on action creators so that its 'dispatch' and 'getState' params are
|
||||||
|
|||||||
124
clientapp/src/store/reducers/BlogCatalog.ts
Normal file
124
clientapp/src/store/reducers/BlogCatalog.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { Action, Reducer } from 'redux'
|
||||||
|
import { AppThunkAction } from '../'
|
||||||
|
|
||||||
|
import { GetBlogCatalogRequestModel } from '../../models/requests'
|
||||||
|
import { GetBlogCatalogResponseModel } from '../../models/responses'
|
||||||
|
import { Get } from '../../restClient'
|
||||||
|
|
||||||
|
export interface BlogCatalogState extends GetBlogCatalogResponseModel {
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestAction extends GetBlogCatalogRequestModel {
|
||||||
|
type: 'REQUEST_BLOG_CATALOG'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReceiveAction extends GetBlogCatalogResponseModel {
|
||||||
|
type: 'RECEIVE_BLOG_CATALOG'
|
||||||
|
}
|
||||||
|
|
||||||
|
type KnownAction = RequestAction | ReceiveAction;
|
||||||
|
|
||||||
|
export const actionCreators = {
|
||||||
|
requestBlogCatalog: (props?: GetBlogCatalogRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
|
||||||
|
|
||||||
|
const apiUrl = 'https://localhost:7151/api/BlogCatalog'
|
||||||
|
|
||||||
|
Get<Promise<GetBlogCatalogResponseModel>>(apiUrl, props)
|
||||||
|
.then(response => response)
|
||||||
|
.then(data => {
|
||||||
|
if(data)
|
||||||
|
dispatch({ type: 'RECEIVE_BLOG_CATALOG', ...data })
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(getState().blogCatalog)
|
||||||
|
|
||||||
|
dispatch({ type: 'REQUEST_BLOG_CATALOG' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unloadedState: BlogCatalogState = {
|
||||||
|
featuredBlog: {
|
||||||
|
id: "",
|
||||||
|
slug: "demo-post",
|
||||||
|
badge: "demo",
|
||||||
|
image: {
|
||||||
|
src: "https://dummyimage.com/850x350/dee2e6/6c757d.jpg",
|
||||||
|
alt: "..."
|
||||||
|
},
|
||||||
|
title: "Lorem ipsum",
|
||||||
|
shortText: "",
|
||||||
|
text: "",
|
||||||
|
author: {
|
||||||
|
id: "",
|
||||||
|
nickName: "Admin",
|
||||||
|
image: {
|
||||||
|
src: "https://dummyimage.com/40x40/ced4da/6c757d",
|
||||||
|
alt: "..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: new Date().toString(),
|
||||||
|
tags: [],
|
||||||
|
|
||||||
|
likes: 0
|
||||||
|
},
|
||||||
|
categories: [
|
||||||
|
{ id: "", text: "" }
|
||||||
|
],
|
||||||
|
|
||||||
|
blogItemsPagination: {
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 1,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
slug: "demo-post",
|
||||||
|
badge: "demo",
|
||||||
|
image: {
|
||||||
|
src: "https://dummyimage.com/850x350/dee2e6/6c757d.jpg",
|
||||||
|
alt: "..."
|
||||||
|
},
|
||||||
|
title: "Lorem ipsum",
|
||||||
|
shortText: "",
|
||||||
|
text: "",
|
||||||
|
author: {
|
||||||
|
id: "",
|
||||||
|
nickName: "Admin",
|
||||||
|
image: {
|
||||||
|
src: "https://dummyimage.com/40x40/ced4da/6c757d",
|
||||||
|
alt: "..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: new Date().toString(),
|
||||||
|
tags: [],
|
||||||
|
|
||||||
|
likes: 0
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer: Reducer<BlogCatalogState> = (state: BlogCatalogState | undefined, incomingAction: Action): BlogCatalogState => {
|
||||||
|
if (state === undefined) {
|
||||||
|
return unloadedState
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = incomingAction as KnownAction
|
||||||
|
switch (action.type) {
|
||||||
|
case 'REQUEST_BLOG_CATALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: true
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'RECEIVE_BLOG_CATALOG':
|
||||||
|
return {
|
||||||
|
...action,
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
@ -1,42 +1,207 @@
|
|||||||
import { Action, Reducer } from 'redux'
|
import { Action, Reducer } from 'redux'
|
||||||
import { AppThunkAction } from '..'
|
import { AppThunkAction } from '../'
|
||||||
import { GetStaticContent, IGetStaticContentRequest, IGetStaticContetnResponse } from '../../controllers/staticContent'
|
|
||||||
|
|
||||||
export interface IContentState extends IGetStaticContetnResponse {
|
import { GetStaticContentRequestModel } from '../../models/requests'
|
||||||
|
import { GetStaticContentResponseModel } from '../../models/responses'
|
||||||
|
import { Get } from '../../restClient'
|
||||||
|
|
||||||
|
export interface ContentState extends GetStaticContentResponseModel {
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestAction extends IGetStaticContentRequest {
|
interface RequestAction extends GetStaticContentRequestModel {
|
||||||
type: 'REQUEST_CONTENT'
|
type: 'REQUEST_CONTENT'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReceiveAction extends IGetStaticContetnResponse {
|
interface ReceiveAction extends GetStaticContentResponseModel {
|
||||||
type: 'RECEIVE_CONTENT'
|
type: 'RECEIVE_CONTENT'
|
||||||
}
|
}
|
||||||
|
|
||||||
type KnownAction = RequestAction | ReceiveAction;
|
type KnownAction = RequestAction | ReceiveAction;
|
||||||
|
|
||||||
export const actionCreators = {
|
export const actionCreators = {
|
||||||
requestContent: (): AppThunkAction<KnownAction> => async (dispatch, getState) => {
|
requestContent: (props?: GetStaticContentRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
|
||||||
|
|
||||||
|
const apiUrl = 'https://localhost:7151/api/StaticContent'
|
||||||
|
|
||||||
|
Get<Promise<GetStaticContentResponseModel>>(apiUrl, props)
|
||||||
|
.then(response => response)
|
||||||
|
.then((data) => {
|
||||||
|
if(data) {
|
||||||
|
dispatch({ type: 'RECEIVE_CONTENT', ...data })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(getState().content)
|
||||||
|
|
||||||
dispatch({ type: 'REQUEST_CONTENT' })
|
dispatch({ type: 'REQUEST_CONTENT' })
|
||||||
|
|
||||||
var fetchData = await GetStaticContent()
|
|
||||||
console.log(fetchData)
|
|
||||||
|
|
||||||
dispatch({ type: 'RECEIVE_CONTENT', ...fetchData })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unloadedState: IContentState = {
|
const unloadedState: ContentState = {
|
||||||
siteName: "MAKS-IT",
|
siteName: "MAKS-IT",
|
||||||
routes: [
|
routes: [
|
||||||
{ target: "/", component: "Home" }
|
{ target: "/", component: "Home" },
|
||||||
|
{ target: "/home", component: "Home" },
|
||||||
|
{ target: "/shop", childRoutes: [
|
||||||
|
{ target: "", component: "ShopCatalog" },
|
||||||
|
{ target: ":page", component: "ShopCatalog" },
|
||||||
|
{ target: ":page" , childRoutes: [
|
||||||
|
{ target: ":slug", component: "ShopItem" }
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
{ target: "/blog", childRoutes: [
|
||||||
|
{ target: "", component: "BlogCatalog" },
|
||||||
|
{ target: ":page", component: "BlogCatalog" },
|
||||||
|
{ target: ":page" , childRoutes: [
|
||||||
|
{ target: ":slug", component: "BlogItem" }
|
||||||
|
]}
|
||||||
|
]}
|
||||||
],
|
],
|
||||||
|
adminRoutes: [],
|
||||||
|
serviceRoutes: [],
|
||||||
|
|
||||||
|
topMenu: [
|
||||||
|
{ target: "/", title: "Home" },
|
||||||
|
{ target: "/shop", title: "Shop" },
|
||||||
|
{ target: "/blog", title: "Blog" }
|
||||||
|
],
|
||||||
|
sideMenu: [],
|
||||||
|
|
||||||
|
homePage: {
|
||||||
|
titleSection: {
|
||||||
|
title: "Hello, World! by Redux",
|
||||||
|
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>`
|
||||||
|
},
|
||||||
|
featuresSection: {
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
testimonialsSection: {
|
||||||
|
items : [
|
||||||
|
{
|
||||||
|
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>.",
|
||||||
|
reviewer: {
|
||||||
|
id: "",
|
||||||
|
image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." },
|
||||||
|
fullName: "Admin",
|
||||||
|
position: "CEO, MAKS-IT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
featuredBlogsSection: {
|
||||||
|
title: "From our blog",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
slug: "blog-post-title",
|
||||||
|
image: { 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: {
|
||||||
|
id: "",
|
||||||
|
image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." },
|
||||||
|
nickName: "Admin"
|
||||||
|
},
|
||||||
|
created: (new Date).toString(),
|
||||||
|
tags: [ "react", "redux", "webapi" ],
|
||||||
|
|
||||||
|
readTime: 10,
|
||||||
|
likes: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
slug: "blog-post-title",
|
||||||
|
image: { 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: {
|
||||||
|
id: "",
|
||||||
|
image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." },
|
||||||
|
nickName: "Admin"
|
||||||
|
},
|
||||||
|
created: (new Date).toString(),
|
||||||
|
tags: [ "react", "redux", "webapi" ],
|
||||||
|
|
||||||
|
readTime: 10,
|
||||||
|
likes: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
slug: "blog-post-title",
|
||||||
|
image: { 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: {
|
||||||
|
id: "",
|
||||||
|
image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." },
|
||||||
|
nickName: "Admin"
|
||||||
|
},
|
||||||
|
created: (new Date).toString(),
|
||||||
|
tags: [ "react", "redux", "webapi" ],
|
||||||
|
|
||||||
|
readTime: 10,
|
||||||
|
likes: 200,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
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.",
|
||||||
|
email: {
|
||||||
|
title: "Sign up",
|
||||||
|
placeHolder: "Email address..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
shopCatalog: {
|
||||||
|
titleSection: {
|
||||||
|
title: "Shop in style",
|
||||||
|
text: "With this shop hompeage template"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
blogCatalog: {
|
||||||
|
titleSection: {
|
||||||
|
title: "Welcome to Blog Home!",
|
||||||
|
text: "A Bootstrap 5 starter layout for your next blog homepage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
isLoading: false
|
isLoading: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reducer: Reducer<IContentState> = (state: IContentState | undefined, incomingAction: Action): IContentState => {
|
export const reducer: Reducer<ContentState> = (state: ContentState | undefined, incomingAction: Action): ContentState => {
|
||||||
if (state === undefined) {
|
if (state === undefined) {
|
||||||
return unloadedState
|
return unloadedState
|
||||||
}
|
}
|
||||||
|
|||||||
52
clientapp/src/store/reducers/Loader.ts
Normal file
52
clientapp/src/store/reducers/Loader.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Action, Reducer } from 'redux'
|
||||||
|
|
||||||
|
// -----------------
|
||||||
|
// STATE - This defines the type of data maintained in the Redux store.
|
||||||
|
|
||||||
|
export interface LoaderState {
|
||||||
|
visible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestAction {
|
||||||
|
type: 'SHOW_LOADER'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReceiveAction {
|
||||||
|
type: 'HIDE_LOADER'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare a 'discriminated union' type. This guarantees that all references to 'type' properties contain one of the
|
||||||
|
// declared type strings (and not any other arbitrary string).
|
||||||
|
export type KnownAction = RequestAction | ReceiveAction
|
||||||
|
|
||||||
|
// ----------------
|
||||||
|
// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
|
||||||
|
// They don't directly mutate state, but they can have external side-effects (such as loading data).
|
||||||
|
|
||||||
|
export const actionCreators = {
|
||||||
|
show: () => ({ type: 'SHOW_LOADER' } as RequestAction),
|
||||||
|
hide: () => ({ type: 'HIDE_LOADER' } as ReceiveAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------
|
||||||
|
// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
|
||||||
|
|
||||||
|
const unloadedState: LoaderState = {
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer: Reducer<LoaderState> = (state: LoaderState | undefined, incomingAction: Action): LoaderState => {
|
||||||
|
if (state === undefined) {
|
||||||
|
return unloadedState
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = incomingAction as KnownAction
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SHOW_LOADER':
|
||||||
|
return { visible: true }
|
||||||
|
case 'HIDE_LOADER':
|
||||||
|
return { visible: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
94
clientapp/src/store/reducers/ShopCatalog.ts
Normal file
94
clientapp/src/store/reducers/ShopCatalog.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { Action, Reducer } from 'redux'
|
||||||
|
import { AppThunkAction } from '../'
|
||||||
|
|
||||||
|
import { GetShopCatalogRequestModel } from '../../models/requests'
|
||||||
|
import { GetShopCatalogResponseModel } from '../../models/responses'
|
||||||
|
import { Get } from '../../restClient'
|
||||||
|
|
||||||
|
export interface ShopCatalogState extends GetShopCatalogResponseModel {
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestAction extends GetShopCatalogRequestModel {
|
||||||
|
type: 'REQUEST_SHOP_CATALOG'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReceiveAction extends GetShopCatalogResponseModel {
|
||||||
|
type: 'RECEIVE_SHOP_CATALOG'
|
||||||
|
}
|
||||||
|
|
||||||
|
type KnownAction = RequestAction | ReceiveAction
|
||||||
|
|
||||||
|
export const actionCreators = {
|
||||||
|
requestShopCatalog: (props?: GetShopCatalogRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
|
||||||
|
|
||||||
|
const apiUrl = 'https://localhost:7151/api/ShopCatalog'
|
||||||
|
|
||||||
|
Get<Promise<GetShopCatalogResponseModel>>(apiUrl, props)
|
||||||
|
.then(response => response)
|
||||||
|
.then(data => {
|
||||||
|
if(data)
|
||||||
|
dispatch({ type: 'RECEIVE_SHOP_CATALOG', ...data })
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch({ type: 'REQUEST_SHOP_CATALOG' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unloadedState: ShopCatalogState = {
|
||||||
|
shopItemsPagination: {
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 1,
|
||||||
|
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
slug: "shop-catalog-item",
|
||||||
|
sku: "SKU-0",
|
||||||
|
image: { 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: {
|
||||||
|
id: '',
|
||||||
|
image: { src: "https://dummyimage.com/40x40/ced4da/6c757d", alt: "..." },
|
||||||
|
nickName: "Admin"
|
||||||
|
},
|
||||||
|
created: (new Date).toString(),
|
||||||
|
|
||||||
|
tags: [ "react", "redux", "webapi" ],
|
||||||
|
|
||||||
|
rating: 4.5,
|
||||||
|
price: 20,
|
||||||
|
newPrice: 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer: Reducer<ShopCatalogState> = (state: ShopCatalogState | undefined, incomingAction: Action): ShopCatalogState => {
|
||||||
|
if (state === undefined) {
|
||||||
|
return unloadedState
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = incomingAction as KnownAction
|
||||||
|
switch (action.type) {
|
||||||
|
case 'REQUEST_SHOP_CATALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: true
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'RECEIVE_SHOP_CATALOG':
|
||||||
|
return {
|
||||||
|
...action,
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
@ -15,8 +15,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProviders", "DataProviders\DataProviders.csproj", "{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProviders", "DataProviders\DataProviders.csproj", "{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{1FE09D24-5FC7-4EDD-AC19-C06DB9C035DB}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -43,10 +41,6 @@ Global
|
|||||||
{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{1FE09D24-5FC7-4EDD-AC19-C06DB9C035DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{1FE09D24-5FC7-4EDD-AC19-C06DB9C035DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{1FE09D24-5FC7-4EDD-AC19-C06DB9C035DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{1FE09D24-5FC7-4EDD-AC19-C06DB9C035DB}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@ -5,21 +5,10 @@ using Core.Models;
|
|||||||
using WeatherForecast.Models;
|
using WeatherForecast.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Core.Abstractions.Models;
|
using Core.Abstractions.Models;
|
||||||
|
using WeatherForecast.Models.Responses;
|
||||||
|
|
||||||
namespace WeatherForecast.Controllers;
|
namespace WeatherForecast.Controllers;
|
||||||
|
|
||||||
#region Input models
|
|
||||||
public class GetBlogCatalogResponse : ResponseModel {
|
|
||||||
|
|
||||||
public BlogItemModel FeaturedBlog { get; set; }
|
|
||||||
|
|
||||||
public List<CategoryModel> Categories { get; set; }
|
|
||||||
|
|
||||||
public PaginationModel<BlogItemModel> BlogItemsPagination { get; set; }
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
@ -41,37 +30,41 @@ public class BlogCatalogController : ControllerBase {
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult Get([FromQuery] Guid? category, [FromQuery] string? searchText, [FromQuery] int currentPage = 1, [FromQuery] int itemsPerPage = 4) {
|
public IActionResult Get([FromQuery] Guid? category, [FromQuery] string? searchText, [FromQuery] int currentPage = 1, [FromQuery] int itemsPerPage = 4) {
|
||||||
var blogItemModel = new BlogItemModel {
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Slug = "blog-post-title",
|
|
||||||
Image = new ImageModel { Src = "https://dummyimage.com/850x350/dee2e6/6c757d.jpg", 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
var blogModels = new List<BlogItemModel>();
|
var blogModels = new List<BlogItemModel>();
|
||||||
for (int i = 0; i < itemsPerPage; i++) {
|
for (int i = 0; i < 100; i++) {
|
||||||
|
var blogItemModel = new BlogItemModel {
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Slug = "blog-post-title",
|
||||||
|
Image = new ImageModel { Src = "https://dummyimage.com/850x350/dee2e6/6c757d.jpg", Alt = "..." },
|
||||||
|
Badge = "news",
|
||||||
|
Title = $"Blog post title {i}",
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
blogModels.Add(blogItemModel);
|
blogModels.Add(blogItemModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
var blogCatalogResponse = new GetBlogCatalogResponse {
|
var totalPages = blogModels.Count() / itemsPerPage;
|
||||||
FeaturedBlog = blogItemModel,
|
|
||||||
|
var blogCatalogResponse = new GetBlogCatalogResponseModel {
|
||||||
|
FeaturedBlog = blogModels[0],
|
||||||
BlogItemsPagination = new PaginationModel<BlogItemModel> {
|
BlogItemsPagination = new PaginationModel<BlogItemModel> {
|
||||||
CurrentPage = currentPage,
|
CurrentPage = currentPage,
|
||||||
TotalPages = 100,
|
TotalPages = totalPages,
|
||||||
Items = blogModels
|
Items = blogModels.Skip((currentPage -1) * itemsPerPage).Take(itemsPerPage).ToList()
|
||||||
},
|
},
|
||||||
Categories = new List<CategoryModel> {
|
Categories = new List<CategoryModel> {
|
||||||
new CategoryModel {
|
new CategoryModel {
|
||||||
|
|||||||
@ -3,16 +3,10 @@ using Core.Models;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using WeatherForecast.Models;
|
using WeatherForecast.Models;
|
||||||
|
using WeatherForecast.Models.Responses;
|
||||||
|
|
||||||
namespace WeatherForecast.Controllers;
|
namespace WeatherForecast.Controllers;
|
||||||
|
|
||||||
#region Response models
|
|
||||||
public class GetShopCatalogResponse : ResponseModel {
|
|
||||||
|
|
||||||
public PaginationModel<ShopItemModel> ShopItemsPagination { get; set; }
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
@ -39,6 +33,7 @@ public class ShopCatalogController : ControllerBase {
|
|||||||
for (int i = 0; i < 8; i++) {
|
for (int i = 0; i < 8; i++) {
|
||||||
var shopItemModel = new ShopItemModel {
|
var shopItemModel = new ShopItemModel {
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
|
Sku = "SKU-0",
|
||||||
Slug = "shop-catalog-item",
|
Slug = "shop-catalog-item",
|
||||||
Image = new ImageModel { Src = "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", Alt = "..." },
|
Image = new ImageModel { Src = "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", Alt = "..." },
|
||||||
Badge = "sale",
|
Badge = "sale",
|
||||||
@ -63,7 +58,7 @@ public class ShopCatalogController : ControllerBase {
|
|||||||
shopModels.Add(shopItemModel);
|
shopModels.Add(shopItemModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
var shopCatalogResponse = new GetShopCatalogResponse {
|
var shopCatalogResponse = new GetShopCatalogResponseModel {
|
||||||
ShopItemsPagination = new PaginationModel<ShopItemModel> {
|
ShopItemsPagination = new PaginationModel<ShopItemModel> {
|
||||||
CurrentPage = currentPage,
|
CurrentPage = currentPage,
|
||||||
TotalPages = 100,
|
TotalPages = 100,
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using WeatherForecast.Models;
|
using WeatherForecast.Models;
|
||||||
|
using WeatherForecast.Models.Abstractions;
|
||||||
|
using WeatherForecast.Models.Pages;
|
||||||
|
using WeatherForecast.Models.PageSections;
|
||||||
|
using WeatherForecast.Models.Responses;
|
||||||
|
|
||||||
namespace WeatherForecast.Controllers;
|
namespace WeatherForecast.Controllers;
|
||||||
|
|
||||||
@ -82,7 +86,7 @@ public class StaticContentController : ControllerBase {
|
|||||||
new MenuItemModel ("Shop", "/shop"),
|
new MenuItemModel ("Shop", "/shop"),
|
||||||
new MenuItemModel ("Blog", "/blog"),
|
new MenuItemModel ("Blog", "/blog"),
|
||||||
new MenuItemModel ("Signin", "/signin"),
|
new MenuItemModel ("Signin", "/signin"),
|
||||||
new MenuItemModel ("Sognout", "/signout")
|
new MenuItemModel ("Signout", "/signout")
|
||||||
};
|
};
|
||||||
|
|
||||||
var sideMenu = new List<MenuItemModel> {
|
var sideMenu = new List<MenuItemModel> {
|
||||||
@ -125,82 +129,84 @@ public class StaticContentController : ControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var pages = new List<object>();
|
var homePage = new HomePageModel {
|
||||||
|
TitleSection = new TitleSectionModel {
|
||||||
pages.Add(new {
|
Title = "Hello, World! by C#",
|
||||||
Id = "HomePage",
|
|
||||||
TitleSection = new {
|
|
||||||
Title = "Hello, World!",
|
|
||||||
Text = @"
|
Text = @"
|
||||||
<p>Welcome to your new single-page application, built with:</p>
|
<p>Welcome to your new single-page application, built with:</p>
|
||||||
<ul>
|
<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://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://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>
|
<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>",
|
</ul>",
|
||||||
Image = new ImageModel { Src = "https://dummyimage.com/600x400/343a40/6c757d", Alt = "..." },
|
Image = new ImageModel { Src = "https://dummyimage.com/600x400/343a40/6c757d", Alt = "..." },
|
||||||
PrimaryLink = new MenuItemModel("Get Started", "#features"),
|
PrimaryLink = new MenuItemModel("Get Started", "#features"),
|
||||||
SecondaryLink = new MenuItemModel("Learn More", "#!")
|
SecondaryLink = new MenuItemModel("Learn More", "#!")
|
||||||
},
|
},
|
||||||
FeaturesSection = new {
|
|
||||||
|
FeaturesSection = new FeaturesSectionModel {
|
||||||
Title = "To help you get started, we have also set up:",
|
Title = "To help you get started, we have also set up:",
|
||||||
Items = new[] {
|
Items = new List<FeatureModel> {
|
||||||
new {
|
new FeatureModel {
|
||||||
Icon = "navigation",
|
Icon = "navigation",
|
||||||
Title = "Client-side navigation",
|
Title = "Client-side navigation",
|
||||||
Text = "For example, click <em>Counter</em> then <em>Back</em> to return here."
|
Text = "For example, click <em>Counter</em> then <em>Back</em> to return here."
|
||||||
},
|
},
|
||||||
new {
|
new FeatureModel {
|
||||||
Icon = "server",
|
Icon = "server",
|
||||||
Title = "Development server integration",
|
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."
|
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 {
|
new FeatureModel {
|
||||||
Icon = "terminal",
|
Icon = "terminal",
|
||||||
Title = "Efficient production builds",
|
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."
|
Text = "In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration produces minified, efficiently bundled JavaScript files."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
TestimonialsSection = new {
|
TestimonialsSection = new TestimonialsSectionModel {
|
||||||
Items = new[] {
|
Items = new List<TestimonialModel> {
|
||||||
new {
|
new TestimonialModel {
|
||||||
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>.",
|
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 {
|
Reviewer = new ReviewerModel {
|
||||||
Image = new ImageModel { Src = "https://dummyimage.com/40x40/ced4da/6c757d", Alt = "..." },
|
Image = new ImageModel { Src = "https://dummyimage.com/40x40/ced4da/6c757d", Alt = "..." },
|
||||||
NickName = "Tom Ato/CEO, Pomodoro"
|
FullName = "Admin",
|
||||||
|
Position = "CEO, MAKS-IT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
FeaturedBlogsSection = new {
|
FeaturedBlogsSection = new FeaturedBologsSectionModel {
|
||||||
Title = "From our blog",
|
Title = "From our blog",
|
||||||
Items = blogItems
|
Items = blogItems
|
||||||
},
|
},
|
||||||
CallToActionSection = new {
|
|
||||||
|
CallToActionSection = new CallToActionSectionModel {
|
||||||
Title = "New products, delivered to you.",
|
Title = "New products, delivered to you.",
|
||||||
Text = "Sign up for our newsletter for the latest updates.",
|
Text = "Sign up for our newsletter for the latest updates.",
|
||||||
PrivacyDisclaimer = "We care about privacy, and will never share your data."
|
PrivacyDisclaimer = "We care about privacy, and will never share your data.",
|
||||||
|
Email = new FormItemModel {
|
||||||
|
PlaceHolder = "Email address...",
|
||||||
|
Title = "Sign up"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
pages.Add(new {
|
var shopCatalogPage = new ShopCatalogPageModel {
|
||||||
Id = "ShopCatalog",
|
TitleSection = new TitleSectionModel {
|
||||||
TitleSection = new {
|
|
||||||
Title = "Shop in style",
|
Title = "Shop in style",
|
||||||
Text = "With this shop hompeage template"
|
Text = "With this shop hompeage template"
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
pages.Add(new {
|
var blogCatalogPage = new BlogCatalogPageModel {
|
||||||
Id = "BlogCatalog",
|
TitleSection = new TitleSectionModel {
|
||||||
TitleSection = new {
|
|
||||||
Title = "Welcome to Blog Home!",
|
Title = "Welcome to Blog Home!",
|
||||||
Text = "A Bootstrap 5 starter layout for your next blog homepage"
|
Text = "A Bootstrap 5 starter layout for your next blog homepage"
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
return Ok(new {
|
return Ok(new GetStaticContentResponseModel {
|
||||||
SiteName = "MAKS-IT",
|
SiteName = "MAKS-IT",
|
||||||
|
|
||||||
Routes = routes,
|
Routes = routes,
|
||||||
@ -209,7 +215,9 @@ public class StaticContentController : ControllerBase {
|
|||||||
|
|
||||||
TopMenu = topMenu,
|
TopMenu = topMenu,
|
||||||
SideMenu = sideMenu,
|
SideMenu = sideMenu,
|
||||||
Pages = pages
|
HomePage = homePage,
|
||||||
|
ShopCatalog = shopCatalogPage,
|
||||||
|
BlogCatalog = blogCatalogPage
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,9 @@
|
|||||||
using Core.Abstractions.Models;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
using WeatherForecast.Models;
|
using WeatherForecast.Models.Responses;
|
||||||
|
|
||||||
namespace WeatherForecast.Controllers;
|
namespace WeatherForecast.Controllers;
|
||||||
|
|
||||||
#region Response models
|
|
||||||
public class GetWeatherForecastResponse : ResponseModel {
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
|
|
||||||
public int TemperatureC { get; set; }
|
|
||||||
|
|
||||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
|
||||||
|
|
||||||
public string? Summary { get; set; }
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("[controller]")]
|
[Route("[controller]")]
|
||||||
public class WeatherForecastController : ControllerBase
|
public class WeatherForecastController : ControllerBase
|
||||||
@ -35,9 +21,9 @@ public class WeatherForecastController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet(Name = "GetWeatherForecast")]
|
[HttpGet(Name = "GetWeatherForecast")]
|
||||||
public IEnumerable<GetWeatherForecastResponse> Get()
|
public IEnumerable<GetWeatherForecastResponseModel> Get()
|
||||||
{
|
{
|
||||||
return Enumerable.Range(1, 5).Select(index => new GetWeatherForecastResponse {
|
return Enumerable.Range(1, 5).Select(index => new GetWeatherForecastResponseModel {
|
||||||
Date = DateTime.Now.AddDays(index),
|
Date = DateTime.Now.AddDays(index),
|
||||||
TemperatureC = Random.Shared.Next(-20, 55),
|
TemperatureC = Random.Shared.Next(-20, 55),
|
||||||
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
|
||||||
WORKDIR /app
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 443
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
|
||||||
WORKDIR /src
|
|
||||||
COPY ["WeatherForecast/WeatherForecast.csproj", "WeatherForecast/"]
|
|
||||||
COPY ["Core/Core.csproj", "Core/"]
|
|
||||||
RUN dotnet restore "WeatherForecast/WeatherForecast.csproj"
|
|
||||||
COPY . .
|
|
||||||
WORKDIR "/src/WeatherForecast"
|
|
||||||
RUN dotnet build "WeatherForecast.csproj" -c Release -o /app/build
|
|
||||||
|
|
||||||
FROM build AS publish
|
|
||||||
RUN dotnet publish "WeatherForecast.csproj" -c Release -o /app/publish
|
|
||||||
|
|
||||||
FROM base AS final
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
ENTRYPOINT ["dotnet", "WeatherForecast.dll"]
|
|
||||||
4
webapi/WeatherForecast/Models/Abstractions/PageModel.cs
Normal file
4
webapi/WeatherForecast/Models/Abstractions/PageModel.cs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
namespace WeatherForecast.Models.Abstractions {
|
||||||
|
public abstract class PageModel { }
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
namespace WeatherForecast.Models.Abstractions {
|
||||||
|
public abstract class PageSectionModel {
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? Text { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
namespace WeatherForecast.Models.Abstractions {
|
||||||
|
public abstract class PersonModel {
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public ImageModel Image { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace WeatherForecast.Models {
|
namespace WeatherForecast.Models.Abstractions {
|
||||||
public abstract class PostItemModel {
|
public abstract class PostItemModel {
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
namespace WeatherForecast.Models {
|
using WeatherForecast.Models.Abstractions;
|
||||||
public class AuthorModel {
|
|
||||||
public Guid Id { get; set; }
|
namespace WeatherForecast.Models {
|
||||||
public ImageModel Image { get; set; }
|
public class AuthorModel : PersonModel {
|
||||||
public string NickName { get; set; }
|
public string NickName { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
namespace WeatherForecast.Models {
|
using WeatherForecast.Models.Abstractions;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models {
|
||||||
public class BlogItemModel : PostItemModel {
|
public class BlogItemModel : PostItemModel {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
7
webapi/WeatherForecast/Models/FeatureModel.cs
Normal file
7
webapi/WeatherForecast/Models/FeatureModel.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace WeatherForecast.Models {
|
||||||
|
public class FeatureModel {
|
||||||
|
public string Icon { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Text { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
6
webapi/WeatherForecast/Models/FormItemModel.cs
Normal file
6
webapi/WeatherForecast/Models/FormItemModel.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace WeatherForecast.Models {
|
||||||
|
public class FormItemModel {
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? PlaceHolder { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
using WeatherForecast.Models.Abstractions;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.PageSections {
|
||||||
|
public class CallToActionSectionModel : PageSectionModel {
|
||||||
|
public string PrivacyDisclaimer { get; set; }
|
||||||
|
|
||||||
|
public FormItemModel Email { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
using WeatherForecast.Models.Abstractions;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.PageSections {
|
||||||
|
public class FeaturedBologsSectionModel : PageSectionModel {
|
||||||
|
public List<BlogItemModel> Items { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
using WeatherForecast.Models.Abstractions;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.PageSections {
|
||||||
|
public class FeaturesSectionModel : PageSectionModel {
|
||||||
|
public List<FeatureModel> Items { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
using WeatherForecast.Models.Abstractions;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.PageSections {
|
||||||
|
public class TestimonialsSectionModel : PageSectionModel {
|
||||||
|
public List<TestimonialModel> Items { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
using WeatherForecast.Models.Abstractions;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.PageSections {
|
||||||
|
public class TitleSectionModel : PageSectionModel {
|
||||||
|
|
||||||
|
public ImageModel? Image { get; set; }
|
||||||
|
public MenuItemModel? PrimaryLink { get; set; }
|
||||||
|
public MenuItemModel? SecondaryLink { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using WeatherForecast.Models.PageSections;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.Pages {
|
||||||
|
public class BlogCatalogPageModel : PageModel {
|
||||||
|
public TitleSectionModel TitleSection { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
webapi/WeatherForecast/Models/Pages/HomePageModel.cs
Normal file
13
webapi/WeatherForecast/Models/Pages/HomePageModel.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using WeatherForecast.Models.Abstractions;
|
||||||
|
using WeatherForecast.Models.PageSections;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.Pages {
|
||||||
|
public class HomePageModel : PageModel{
|
||||||
|
public TitleSectionModel TitleSection { get; set; }
|
||||||
|
public FeaturesSectionModel FeaturesSection { get; set; }
|
||||||
|
public TestimonialsSectionModel TestimonialsSection { get; set; }
|
||||||
|
public FeaturedBologsSectionModel FeaturedBlogsSection { get; set; }
|
||||||
|
public CallToActionSectionModel CallToActionSection { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
using WeatherForecast.Models.Abstractions;
|
||||||
|
using WeatherForecast.Models.PageSections;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.Pages {
|
||||||
|
public class ShopCatalogPageModel : PageModel {
|
||||||
|
public TitleSectionModel TitleSection { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
using Core.Abstractions.Models;
|
||||||
|
using Core.Models;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.Responses {
|
||||||
|
public class GetBlogCatalogResponseModel : ResponseModel {
|
||||||
|
|
||||||
|
public BlogItemModel FeaturedBlog { get; set; }
|
||||||
|
|
||||||
|
public List<CategoryModel> Categories { get; set; }
|
||||||
|
|
||||||
|
public PaginationModel<BlogItemModel> BlogItemsPagination { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
using Core.Abstractions.Models;
|
||||||
|
using Core.Models;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.Responses {
|
||||||
|
public class GetShopCatalogResponseModel : ResponseModel {
|
||||||
|
public PaginationModel<ShopItemModel> ShopItemsPagination { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
using Core.Abstractions.Models;
|
||||||
|
using WeatherForecast.Models.Pages;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.Responses {
|
||||||
|
public class GetStaticContentResponseModel : ResponseModel {
|
||||||
|
public string SiteName { get; set; }
|
||||||
|
|
||||||
|
public List<RouteModel> Routes { get; set; }
|
||||||
|
public List<RouteModel> AdminRoutes { get; set; }
|
||||||
|
public List<RouteModel> ServiceRoutes { get; set; }
|
||||||
|
|
||||||
|
public List<MenuItemModel> TopMenu { get; set; }
|
||||||
|
public List<MenuItemModel> SideMenu { get; set; }
|
||||||
|
|
||||||
|
public HomePageModel HomePage { get; set; }
|
||||||
|
public ShopCatalogPageModel ShopCatalog { get; set; }
|
||||||
|
public BlogCatalogPageModel BlogCatalog { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
using Core.Abstractions.Models;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models.Responses {
|
||||||
|
public class GetWeatherForecastResponseModel : ResponseModel {
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureC { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||||
|
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
8
webapi/WeatherForecast/Models/ReviewerModel.cs
Normal file
8
webapi/WeatherForecast/Models/ReviewerModel.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using WeatherForecast.Models.Abstractions;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models {
|
||||||
|
public class ReviewerModel : PersonModel {
|
||||||
|
public string FullName { get; set; }
|
||||||
|
public string Position { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,6 @@
|
|||||||
namespace WeatherForecast.Models {
|
using WeatherForecast.Models.Abstractions;
|
||||||
|
|
||||||
|
namespace WeatherForecast.Models {
|
||||||
public class ShopItemModel : PostItemModel {
|
public class ShopItemModel : PostItemModel {
|
||||||
public List<ImageModel> Images { get; set; }
|
public List<ImageModel> Images { get; set; }
|
||||||
public string Sku { get; set; }
|
public string Sku { get; set; }
|
||||||
|
|||||||
6
webapi/WeatherForecast/Models/TestimonialModel.cs
Normal file
6
webapi/WeatherForecast/Models/TestimonialModel.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace WeatherForecast.Models {
|
||||||
|
public class TestimonialModel {
|
||||||
|
public string Text { get; set; }
|
||||||
|
public ReviewerModel Reviewer { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,9 +6,6 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<RootNamespace>WeatherForecast</RootNamespace>
|
<RootNamespace>WeatherForecast</RootNamespace>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<UserSecretsId>2ea970dd-e71a-4c8e-9ff6-2d1d3123d4df</UserSecretsId>
|
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
|
||||||
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -21,4 +18,8 @@
|
|||||||
<ProjectReference Include="..\Core\Core.csproj" />
|
<ProjectReference Include="..\Core\Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Models\Requests\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user