(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
|
||||
import { DynamicLayout } from './layouts'
|
||||
import { DynamicPage } from './pages'
|
||||
import { IRouteModel } from './models'
|
||||
import { RouteModel } from './models'
|
||||
import { ApplicationState } from './store'
|
||||
import { Loader } from './components/Loader'
|
||||
|
||||
|
||||
interface IRouteProp {
|
||||
@ -18,10 +19,10 @@ interface IRouteProp {
|
||||
element?: JSX.Element
|
||||
}
|
||||
|
||||
const NestedRoutes = (routes: IRouteModel[], tag: string | undefined = undefined) => {
|
||||
const NestedRoutes = (routes: RouteModel[], tag: string | undefined = undefined) => {
|
||||
if(!Array.isArray(routes)) return
|
||||
|
||||
return routes.map((route: IRouteModel, index: number) => {
|
||||
return routes.map((route: RouteModel, index: number) => {
|
||||
const routeProps: IRouteProp = {
|
||||
path: route.target
|
||||
}
|
||||
@ -37,10 +38,12 @@ const NestedRoutes = (routes: IRouteModel[], tag: string | undefined = undefined
|
||||
})
|
||||
}
|
||||
|
||||
const App: FC = () => {
|
||||
const App = () => {
|
||||
const { pathname } = useLocation()
|
||||
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(() => {
|
||||
dispatch(settingsActionCreators.requestContent())
|
||||
@ -49,16 +52,18 @@ const App: FC = () => {
|
||||
useEffect(() => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
behavior: 'auto',
|
||||
})
|
||||
}, [pathname])
|
||||
|
||||
return <>
|
||||
<Routes>
|
||||
{state?.routes ? NestedRoutes(state.routes, 'PublicLayout') : ''}
|
||||
{state?.adminRoutes ? NestedRoutes(state.adminRoutes, 'AdminLayout') : ''}
|
||||
{state?.serviceRoutes ? NestedRoutes(state.serviceRoutes) : ''}
|
||||
{content?.routes ? NestedRoutes(content.routes, 'PublicLayout') : ''}
|
||||
{content?.adminRoutes ? NestedRoutes(content.adminRoutes, 'AdminLayout') : ''}
|
||||
{content?.serviceRoutes ? NestedRoutes(content.serviceRoutes) : ''}
|
||||
</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 { findRoutes } from './findRoutes'
|
||||
import { getKeyValue } from './getKeyValue'
|
||||
|
||||
export {
|
||||
getKeyValue,
|
||||
dateFormat
|
||||
dateFormat,
|
||||
findRoutes
|
||||
}
|
||||
@ -1,12 +1,13 @@
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Container } from 'reactstrap'
|
||||
import { ApplicationState } from '../../../store'
|
||||
|
||||
const Footer = () => {
|
||||
// let { siteName } = useSelector((state: IReduxState) => state.settings)
|
||||
const content = useSelector((state: ApplicationState) => state.content)
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
|
||||
@ -3,14 +3,13 @@ import { Link } from 'react-router-dom'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Collapse, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'
|
||||
import { FeatherIcon } from '../../../components/FeatherIcons'
|
||||
import { ApplicationState } from '../../../store'
|
||||
import { MenuItemModel } from '../../../models'
|
||||
|
||||
|
||||
const NavMenu : FC = () => {
|
||||
/*
|
||||
let { siteName, topMenu = [] } = useSelector((state: IReduxState) => {
|
||||
return state.settings
|
||||
})
|
||||
|
||||
const content = useSelector((state: ApplicationState) => state.content)
|
||||
|
||||
const [state, hookState] = useState({
|
||||
isOpen: false
|
||||
})
|
||||
@ -20,23 +19,21 @@ const NavMenu : FC = () => {
|
||||
isOpen: !state.isOpen
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
return <header>
|
||||
{/**
|
||||
<Navbar className="navbar-expand-sm navbar-toggleable-sm fixed-top border-bottom box-shadow mb-3 bg-light">
|
||||
<NavbarBrand href="/">{siteName}</ NavbarBrand>
|
||||
<NavbarBrand href="/">{content?.siteName}</ NavbarBrand>
|
||||
<NavbarToggler onClick={toggle} className="mr-2"/>
|
||||
<Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={state.isOpen} navbar>
|
||||
<ul className="navbar-nav flex-grow">
|
||||
{topMenu.map((item: IMenuItemModel, index: number) => {
|
||||
{content?.topMenu ? content.topMenu.map((item: MenuItemModel, index: number) => {
|
||||
return <NavItem key={index}>
|
||||
<NavLink tag={Link} className="text-dark" to={item.target}>
|
||||
{item.icon ? <FeatherIcon icon={item.icon}/> : ''}
|
||||
{item.title}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
})}
|
||||
}) : ''}
|
||||
</ul>
|
||||
</Collapse>
|
||||
|
||||
@ -47,7 +44,6 @@ const NavMenu : FC = () => {
|
||||
</button>
|
||||
</form>
|
||||
</Navbar>
|
||||
*/}
|
||||
</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 {
|
||||
src: string,
|
||||
alt: string
|
||||
}
|
||||
import { PageSectionModel, PersonModel, PostItemModel } from "./abstractions"
|
||||
|
||||
export interface IAuthorModel {
|
||||
id: string,
|
||||
image?: IImageModel,
|
||||
export interface AuthorModel extends PersonModel {
|
||||
nickName: string
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
export interface BlogItemModel extends PostItemModel {
|
||||
readTime?: number,
|
||||
likes?: number
|
||||
}
|
||||
|
||||
export interface IShopItemModel extends IPostItemModel {
|
||||
images?: IImageModel [],
|
||||
export interface CategoryModel {
|
||||
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,
|
||||
rating?: number,
|
||||
price: number,
|
||||
@ -37,38 +51,19 @@ export interface IShopItemModel extends IPostItemModel {
|
||||
quantity?: number
|
||||
}
|
||||
|
||||
export interface TestimonialsModel {
|
||||
text: string,
|
||||
reviewer: ReviewerModel
|
||||
}
|
||||
|
||||
interface IPostPaginationModel {
|
||||
export interface PaginationModel<T> {
|
||||
totalPages: number,
|
||||
currentPage: number,
|
||||
totalPages: number
|
||||
items: T []
|
||||
}
|
||||
|
||||
export interface IBlogItemsPaginationModel extends IPostPaginationModel {
|
||||
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,
|
||||
export interface FormItemModel {
|
||||
title?: string,
|
||||
target?: string
|
||||
childItems?: IMenuItemModel []
|
||||
placeHolder?: string
|
||||
}
|
||||
|
||||
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 { Link } from 'react-router-dom'
|
||||
import { Card, CardBody, CardFooter, CardHeader, CardImg, Col, Container, Row } from 'reactstrap'
|
||||
import { dateFormat } from '../../../functions'
|
||||
import React, { FC, useEffect } from 'react'
|
||||
|
||||
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 { IBlogItemModel, IBlogItemsPaginationModel } from '../../../models'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
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 { 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
|
||||
|
||||
return <Card className="mb-4 shadow border-0">
|
||||
<CardImg top {...image} />
|
||||
<CardBody className="p-4">
|
||||
<div className="badge bg-primary bg-gradient rounded-pill mb-2">{badge}</div>
|
||||
<Link className="text-decoration-none link-dark stretched-link" to={`blog/item/${slug}`}>
|
||||
<Link className="text-decoration-none link-dark stretched-link" to={`/blog/${slug}`}>
|
||||
<h5 className="card-title mb-3">{title}</h5>
|
||||
</Link>
|
||||
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: shortText }}></p>
|
||||
@ -38,8 +54,15 @@ const FeaturedBlog: FC<IBlogItemModel> = (props) => {
|
||||
|
||||
}
|
||||
|
||||
const BlogItemsPagination: FC<IBlogItemsPaginationModel> = (props) => {
|
||||
const { items, currentPage, totalPages } = props
|
||||
interface BlogItemsPaginationModel extends PaginationModel<BlogItemModel> {
|
||||
path: string
|
||||
}
|
||||
|
||||
const BlogItemsPagination: FC<BlogItemsPaginationModel> = (props) => {
|
||||
const { items, currentPage, totalPages, path } = props
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return <>
|
||||
{items.map((item, index) => <Col key={index} className="lg-6">
|
||||
@ -48,61 +71,66 @@ const BlogItemsPagination: FC<IBlogItemsPaginationModel> = (props) => {
|
||||
<CardImg top {...item.image} />
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
</Card>
|
||||
</Col>)}
|
||||
|
||||
<nav aria-label="Pagination">
|
||||
<hr className="my-0" />
|
||||
<ul className="pagination justify-content-center my-4">
|
||||
<li className="page-item disabled"><a className="page-link" href="#" aria-disabled="true">Newer</a></li>
|
||||
<li className="page-item active" aria-current="page"><a className="page-link" href="#!">1</a></li>
|
||||
<li className="page-item"><a className="page-link" href="#!">2</a></li>
|
||||
<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>
|
||||
<li className="page-item"><a className="page-link" href="#!">Older</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<Pagination {...{
|
||||
totalPages: totalPages,
|
||||
currentPage: currentPage,
|
||||
onClick: (nextPage) => {
|
||||
dispatch(blogCatalogActionCreators.requestBlogCatalog({
|
||||
currentPage: nextPage + ""
|
||||
}))
|
||||
|
||||
navigate(`${path}/${nextPage}`)
|
||||
}
|
||||
}} />
|
||||
</>
|
||||
}
|
||||
|
||||
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(() => {
|
||||
GetBlogCatalog().then(response => {
|
||||
setState(response)
|
||||
})
|
||||
dispatch(blogCatalogActionCreators.requestBlogCatalog({
|
||||
currentPage: params?.page ? params.page : "1"
|
||||
}))
|
||||
}, [])
|
||||
|
||||
return <>
|
||||
<header className="py-5 bg-light border-bottom mb-4">
|
||||
<Container fluid>
|
||||
<div className="text-center my-5">
|
||||
<h1 className="fw-bolder">Welcome to Blog Home!</h1>
|
||||
<p className="lead mb-0">A Bootstrap 5 starter layout for your next blog homepage</p>
|
||||
</div>
|
||||
</Container>
|
||||
</header>
|
||||
useEffect(() => {
|
||||
blogCatalog?.isLoading
|
||||
? dispatch(loaderActionCreators.show())
|
||||
: setTimeout(() => {
|
||||
dispatch(loaderActionCreators.hide())
|
||||
}, 1000)
|
||||
}, [blogCatalog?.isLoading])
|
||||
|
||||
return <>
|
||||
<TitleSection {...page?.titleSection} />
|
||||
<Container fluid>
|
||||
<Row>
|
||||
<Col>
|
||||
{state?.featuredBlog ? <FeaturedBlog {...state.featuredBlog} /> : ''}
|
||||
{blogCatalog?.featuredBlog ? <FeaturedBlog {...blogCatalog.featuredBlog} /> : ''}
|
||||
<Row>
|
||||
{state?.blogItemsPagination ? <BlogItemsPagination {...state.blogItemsPagination} /> : '' }
|
||||
{blogCatalog?.blogItemsPagination ? <BlogItemsPagination path={path} {...blogCatalog.blogItemsPagination} /> : '' }
|
||||
</Row>
|
||||
</Col>
|
||||
<Col lg="4">
|
||||
<Search />
|
||||
{state?.categories ? <Categories {...{
|
||||
categories: state.categories
|
||||
{blogCatalog?.categories ? <Categories {...{
|
||||
categories: blogCatalog.categories
|
||||
}} /> : '' }
|
||||
<Empty/>
|
||||
</Col>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { Card, CardBody, CardHeader, Col, Row } from 'reactstrap'
|
||||
import { ICategoryModel } from '../../../models'
|
||||
import { CategoryModel } from '../../../models'
|
||||
|
||||
const Search = () => {
|
||||
return <Card className="mb-4">
|
||||
@ -14,8 +14,8 @@ const Search = () => {
|
||||
</Card>
|
||||
}
|
||||
|
||||
export interface ICategories {
|
||||
categories?: ICategoryModel []
|
||||
interface ICategories {
|
||||
categories?: CategoryModel []
|
||||
}
|
||||
|
||||
const Categories = (props: ICategories) => {
|
||||
@ -27,7 +27,7 @@ const Categories = (props: ICategories) => {
|
||||
|
||||
const middleIndex = Math.ceil(categories.length / 2)
|
||||
|
||||
const firstHalf = categories.splice(0, middleIndex)
|
||||
const firstHalf = categories.splice(0, middleIndex)
|
||||
const secondHalf = categories.splice(-middleIndex)
|
||||
|
||||
return <Card className="mb-4">
|
||||
|
||||
@ -5,6 +5,7 @@ import { Link, useLocation, useParams } from 'react-router-dom'
|
||||
// Redux
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { actionCreators as weatherForecastsActionCreators, WeatherForecast, WeatherForecastsState } from '../store/reducers/WeatherForecasts'
|
||||
import { dateFormat } from '../functions'
|
||||
|
||||
interface IReduxState {
|
||||
weatherForecasts: WeatherForecastsState
|
||||
@ -45,7 +46,7 @@ const FetchData = () => {
|
||||
<tbody>
|
||||
{forecasts.map((forecast: WeatherForecast) =>
|
||||
<tr key={forecast.date}>
|
||||
<td>{forecast.date}</td>
|
||||
<td>{dateFormat(forecast.date)}</td>
|
||||
<td>{forecast.temperatureC}</td>
|
||||
<td>{forecast.temperatureF}</td>
|
||||
<td>{forecast.summary}</td>
|
||||
|
||||
@ -1,56 +1,27 @@
|
||||
import React, { FC } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
// React
|
||||
import React, { FC, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
|
||||
import { FeatherIcon } from '../../components/FeatherIcons'
|
||||
import { IPageModel, IBlogItemModel, IImageModel } from '../../models'
|
||||
|
||||
// Redux
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
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'
|
||||
|
||||
interface ITitleSection {
|
||||
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 TitleSection : FC<TitleSectionModel> = (props) => {
|
||||
const { title, text } = props
|
||||
|
||||
return <header className="py-5 bg-dark">
|
||||
@ -59,7 +30,7 @@ const TitleSection : FC<ITitleSection> = (props) => {
|
||||
<Col className="lg-8 xl-7 xxl-6">
|
||||
<div className="my-5 text-center text-xl-start">
|
||||
<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>
|
||||
<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
|
||||
|
||||
return <section className="py-5" id="features">
|
||||
<Container fluid className="px-5 my-5">
|
||||
<Row className="gx-5">
|
||||
<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 className="lg-8">
|
||||
<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`}>
|
||||
<FeatherIcon icon={item.icon} />
|
||||
</div>
|
||||
<h2 className="h5">{item.title}</h2>
|
||||
<p className="mb-0" dangerouslySetInnerHTML={{ __html: item.text }}></p>
|
||||
</Col>)}
|
||||
</Col>) : ''}
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
}
|
||||
|
||||
const TestimonialsSection: FC<TestimonialsSectionModel> = (props) => {
|
||||
const item = props?.items ? props?.items.shift() : undefined
|
||||
|
||||
|
||||
const TestimonialsSection: FC<ITestimonialsSection> = (props) => {
|
||||
const { text, image } = props
|
||||
if(!item) return <></>
|
||||
|
||||
return <section className="py-5 bg-light">
|
||||
<Container fluid className="px-5 my-5">
|
||||
<Row className="gx-5 justify-content-center">
|
||||
<Col className="lg-10 xl-7">
|
||||
<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">
|
||||
<img className="rounded-circle me-3" {...image} />
|
||||
<div className="fw-bold">Tom Ato<span className="fw-bold text-primary mx-1">/</span>CEO, Pomodoro
|
||||
<img className="rounded-circle me-3" {...item.reviewer.image} />
|
||||
<div className="fw-bold">{item.reviewer.fullName}<span className="fw-bold text-primary mx-1">/</span>{item.reviewer.position}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -126,12 +96,7 @@ const TestimonialsSection: FC<ITestimonialsSection> = (props) => {
|
||||
</section>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const FromOurBlogSection: FC<IFeaturedBlogsSection> = (props) => {
|
||||
const FromOurBlogSection: FC<FeaturedBlogsSectionModel> = (props) => {
|
||||
const { title, text, items } = props
|
||||
|
||||
return <section className="py-5">
|
||||
@ -139,13 +104,13 @@ const FromOurBlogSection: FC<IFeaturedBlogsSection> = (props) => {
|
||||
<Row className="gx-5 justify-content-center">
|
||||
<Col className="lg-8 xl-6">
|
||||
<div className="text-center">
|
||||
<h2 className="fw-bolder">{title}</h2>
|
||||
<p className="lead fw-normal text-muted mb-5" dangerouslySetInnerHTML={{ __html: text }}></p>
|
||||
<h2 className="fw-bolder">{title ? title : ''}</h2>
|
||||
<p className="lead fw-normal text-muted mb-5" dangerouslySetInnerHTML={{ __html: text ? text : '' }}></p>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<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">
|
||||
<CardImg top {...item.image} />
|
||||
<CardBody className="p-4">
|
||||
@ -153,7 +118,7 @@ const FromOurBlogSection: FC<IFeaturedBlogsSection> = (props) => {
|
||||
<Link className="text-decoration-none link-dark stretched-link" to="#!">
|
||||
<h5 className="card-title mb-3">{item.title}</h5>
|
||||
</Link>
|
||||
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: text }}></p>
|
||||
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: text ? text : '' }}></p>
|
||||
</CardBody>
|
||||
<CardFooter className="p-4 pt-0 bg-transparent border-top-0">
|
||||
<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} />
|
||||
<div className="small">
|
||||
<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>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Col>)}
|
||||
</Col>) : ''}
|
||||
</Row>
|
||||
</Container>
|
||||
</section>
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CallToActionSection: FC<ICallToActionSection> = (props) => {
|
||||
const { title, text, privacyDisclaimer } = props
|
||||
const CallToActionSection: FC<CallToActionSectionModel> = (props) => {
|
||||
const { title, text, privacyDisclaimer, email } = props
|
||||
|
||||
return <section className="py-5">
|
||||
<Container fluid className="px-5 my-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 className="ms-xl-4">
|
||||
<div className="input-group mb-2">
|
||||
<input className="form-control" type="text" placeholder="Email address..." aria-label="Email address..." aria-describedby="button-newsletter" />
|
||||
<button className="btn btn-outline-light" id="button-newsletter" type="button">Sign up</button>
|
||||
<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">{email.title ? email.title : ''}</button>
|
||||
</div>
|
||||
<div className="small text-white-50">{privacyDisclaimer}</div>
|
||||
</div>
|
||||
@ -199,16 +163,27 @@ const CallToActionSection: FC<ICallToActionSection> = (props) => {
|
||||
}
|
||||
|
||||
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 <>
|
||||
{ page?.titleSection ? <TitleSection {...page.titleSection}/> : '' }
|
||||
{ page?.featuresSection ? <FeaturesSection {...page.featuresSection} /> : '' }
|
||||
{ page?.testimonialsSection ? <TestimonialsSection {...page.testimonialsSection} /> : '' }
|
||||
{ page?.featuredBlogsSection ? <FromOurBlogSection {...page.featuredBlogsSection} /> : '' }
|
||||
{ page?.callToActionSection ? <CallToActionSection {...page.callToActionSection} /> :'' }
|
||||
<TitleSection {...page.titleSection} />
|
||||
<FeaturesSection {...page.featuresSection} />
|
||||
<TestimonialsSection {...page.testimonialsSection} />
|
||||
<FromOurBlogSection {...page.featuredBlogsSection} />
|
||||
<CallToActionSection {...page.callToActionSection} />
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,50 @@
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
// React
|
||||
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'
|
||||
|
||||
// Models (interfaces)
|
||||
import { PaginationModel, ShopItemModel } from '../../../models'
|
||||
import { TitleSectionModel } from '../../../models/pageSections'
|
||||
|
||||
// Custom components
|
||||
import { FeatherRating } from '../../../components/FeatherRating'
|
||||
import { Pagination } from '../../../components/Pagination'
|
||||
|
||||
import { IShopItemsPaginationModel } from '../../../models'
|
||||
import { IGetShopCatalogResponse, GetShopCatalog } from '../../../controllers/shopCatalog'
|
||||
// Functions
|
||||
import { findRoutes } from '../../../functions'
|
||||
|
||||
const TitleSection: FC<TitleSectionModel> = (props) => {
|
||||
const { title, text } = props
|
||||
|
||||
const ShopItemsPagination: FC<IShopItemsPaginationModel> = (props) => {
|
||||
const { items, currentPage, totalPages } = props
|
||||
return <header className="bg-dark py-5">
|
||||
<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">
|
||||
<Container fluid className="px-4 px-lg-5 mt-5">
|
||||
@ -18,7 +53,7 @@ const ShopItemsPagination: FC<IShopItemsPaginationModel> = (props) => {
|
||||
<Card className="h-100">
|
||||
<div className="badge bg-dark text-white position-absolute" style={{top: "0.5rem", right: "0.5rem"}}>{item.badge}</div>
|
||||
|
||||
<Link to={`${currentPage}/${item.slug}`}>
|
||||
<Link to={`${path}/${currentPage}/${item.slug}`}>
|
||||
<CardImg top {...item.image} />
|
||||
</Link>
|
||||
|
||||
@ -43,82 +78,52 @@ const ShopItemsPagination: FC<IShopItemsPaginationModel> = (props) => {
|
||||
</Col>
|
||||
|
||||
)}
|
||||
|
||||
|
||||
</Row>
|
||||
|
||||
<Pagination {...{
|
||||
totalPages: totalPages,
|
||||
currentPage: currentPage,
|
||||
onClick: (nextPage) => {
|
||||
dispatch(shopCatalogActionCreators.requestShopCatalog({
|
||||
currentPage: nextPage + ""
|
||||
}))
|
||||
|
||||
navigate(`${path}/${nextPage}`)
|
||||
}
|
||||
}} />
|
||||
</Container>
|
||||
</section>
|
||||
}
|
||||
|
||||
|
||||
const ShopCatalog = () => {
|
||||
const params = useParams()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: "1",
|
||||
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 content = useSelector((state: ApplicationState) => state.content)
|
||||
const page = content?.shopCatalog
|
||||
const path = findRoutes(content?.routes, 'ShopCatalog')[0]?.targets[0]
|
||||
|
||||
const shopCatalog = useSelector((state: ApplicationState) => state.shopCatalog)
|
||||
useEffect(() => {
|
||||
GetShopCatalog().then(response => {
|
||||
setState(response)
|
||||
})
|
||||
dispatch(shopCatalogActionCreators.requestShopCatalog({
|
||||
currentPage: params?.page ? params.page : "1"
|
||||
}))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
shopCatalog?.isLoading
|
||||
? dispatch(loaderActionCreators.show())
|
||||
: setTimeout(() => {
|
||||
dispatch(loaderActionCreators.hide())
|
||||
}, 1000)
|
||||
}, [shopCatalog?.isLoading])
|
||||
|
||||
return <>
|
||||
<header className="bg-dark py-5">
|
||||
<Container fluid className="px-4 px-lg-5 my-5">
|
||||
|
||||
<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} /> : ''}
|
||||
<TitleSection {...page?.titleSection} />
|
||||
{shopCatalog?.shopItemsPagination ? <ShopItemsPagination path={path} {...shopCatalog.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)
|
||||
|
||||
if(props) {
|
||||
@ -40,7 +40,10 @@ const Get = async <TResponse>(apiUrl: string, props?: IRequest): Promise<TRespon
|
||||
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 = () => {
|
||||
|
||||
@ -1,12 +1,23 @@
|
||||
import * as WeatherForecasts from './reducers/WeatherForecasts'
|
||||
import * as Counter from './reducers/Counter'
|
||||
|
||||
import * as Loader from './reducers/Loader'
|
||||
|
||||
import * as Content from './reducers/Content'
|
||||
import * as BlogCatalog from './reducers/BlogCatalog'
|
||||
import * as ShopCatalog from './reducers/ShopCatalog'
|
||||
|
||||
|
||||
// The top-level state object
|
||||
export interface ApplicationState {
|
||||
counter: Counter.CounterState | 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
|
||||
@ -15,7 +26,12 @@ export interface ApplicationState {
|
||||
export const reducers = {
|
||||
counter: Counter.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
|
||||
|
||||
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 { AppThunkAction } from '..'
|
||||
import { GetStaticContent, IGetStaticContentRequest, IGetStaticContetnResponse } from '../../controllers/staticContent'
|
||||
import { AppThunkAction } from '../'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
interface RequestAction extends IGetStaticContentRequest {
|
||||
interface RequestAction extends GetStaticContentRequestModel {
|
||||
type: 'REQUEST_CONTENT'
|
||||
}
|
||||
|
||||
interface ReceiveAction extends IGetStaticContetnResponse {
|
||||
interface ReceiveAction extends GetStaticContentResponseModel {
|
||||
type: 'RECEIVE_CONTENT'
|
||||
}
|
||||
|
||||
type KnownAction = RequestAction | ReceiveAction;
|
||||
|
||||
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' })
|
||||
|
||||
var fetchData = await GetStaticContent()
|
||||
console.log(fetchData)
|
||||
|
||||
dispatch({ type: 'RECEIVE_CONTENT', ...fetchData })
|
||||
}
|
||||
}
|
||||
|
||||
const unloadedState: IContentState = {
|
||||
const unloadedState: ContentState = {
|
||||
siteName: "MAKS-IT",
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
return unloadedState
|
||||
}
|
||||
@ -57,4 +222,4 @@ export const reducer: Reducer<IContentState> = (state: IContentState | undefined
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProviders", "DataProviders\DataProviders.csproj", "{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}"
|
||||
EndProject
|
||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{1FE09D24-5FC7-4EDD-AC19-C06DB9C035DB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@ -5,21 +5,10 @@ using Core.Models;
|
||||
using WeatherForecast.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Core.Abstractions.Models;
|
||||
using WeatherForecast.Models.Responses;
|
||||
|
||||
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]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
@ -41,37 +30,41 @@ public class BlogCatalogController : ControllerBase {
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
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>();
|
||||
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);
|
||||
}
|
||||
|
||||
var blogCatalogResponse = new GetBlogCatalogResponse {
|
||||
FeaturedBlog = blogItemModel,
|
||||
var totalPages = blogModels.Count() / itemsPerPage;
|
||||
|
||||
var blogCatalogResponse = new GetBlogCatalogResponseModel {
|
||||
FeaturedBlog = blogModels[0],
|
||||
BlogItemsPagination = new PaginationModel<BlogItemModel> {
|
||||
CurrentPage = currentPage,
|
||||
TotalPages = 100,
|
||||
Items = blogModels
|
||||
TotalPages = totalPages,
|
||||
Items = blogModels.Skip((currentPage -1) * itemsPerPage).Take(itemsPerPage).ToList()
|
||||
},
|
||||
Categories = new List<CategoryModel> {
|
||||
new CategoryModel {
|
||||
|
||||
@ -3,16 +3,10 @@ using Core.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WeatherForecast.Models;
|
||||
using WeatherForecast.Models.Responses;
|
||||
|
||||
namespace WeatherForecast.Controllers;
|
||||
|
||||
#region Response models
|
||||
public class GetShopCatalogResponse : ResponseModel {
|
||||
|
||||
public PaginationModel<ShopItemModel> ShopItemsPagination { get; set; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
[AllowAnonymous]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
@ -39,6 +33,7 @@ public class ShopCatalogController : ControllerBase {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
var shopItemModel = new ShopItemModel {
|
||||
Id = Guid.NewGuid(),
|
||||
Sku = "SKU-0",
|
||||
Slug = "shop-catalog-item",
|
||||
Image = new ImageModel { Src = "https://dummyimage.com/450x300/dee2e6/6c757d.jpg", Alt = "..." },
|
||||
Badge = "sale",
|
||||
@ -63,7 +58,7 @@ public class ShopCatalogController : ControllerBase {
|
||||
shopModels.Add(shopItemModel);
|
||||
}
|
||||
|
||||
var shopCatalogResponse = new GetShopCatalogResponse {
|
||||
var shopCatalogResponse = new GetShopCatalogResponseModel {
|
||||
ShopItemsPagination = new PaginationModel<ShopItemModel> {
|
||||
CurrentPage = currentPage,
|
||||
TotalPages = 100,
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WeatherForecast.Models;
|
||||
using WeatherForecast.Models.Abstractions;
|
||||
using WeatherForecast.Models.Pages;
|
||||
using WeatherForecast.Models.PageSections;
|
||||
using WeatherForecast.Models.Responses;
|
||||
|
||||
namespace WeatherForecast.Controllers;
|
||||
|
||||
@ -82,7 +86,7 @@ public class StaticContentController : ControllerBase {
|
||||
new MenuItemModel ("Shop", "/shop"),
|
||||
new MenuItemModel ("Blog", "/blog"),
|
||||
new MenuItemModel ("Signin", "/signin"),
|
||||
new MenuItemModel ("Sognout", "/signout")
|
||||
new MenuItemModel ("Signout", "/signout")
|
||||
};
|
||||
|
||||
var sideMenu = new List<MenuItemModel> {
|
||||
@ -125,82 +129,84 @@ public class StaticContentController : ControllerBase {
|
||||
}
|
||||
|
||||
|
||||
var pages = new List<object>();
|
||||
|
||||
pages.Add(new {
|
||||
Id = "HomePage",
|
||||
TitleSection = new {
|
||||
Title = "Hello, World!",
|
||||
var homePage = new HomePageModel {
|
||||
TitleSection = new TitleSectionModel {
|
||||
Title = "Hello, World! by C#",
|
||||
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>",
|
||||
<p>Welcome to your new single-page application, built with:</p>
|
||||
<ul>
|
||||
<li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'>C#</a> for cross-platform server-side code</li>
|
||||
<li><a href='https://facebook.github.io/react/'>React</a> and <a href='https://redux.js.org/'>Redux</a> for client-side code</li>
|
||||
<li><a href='https://getbootstrap.com/'>Bootstrap</a>, <a href='https://reactstrap.github.io/?path=/story/home-installation--page'>Reactstrap</a> and <a href=\""https://feathericons.com/\"">Feather icons</a> for layout and styling</li>
|
||||
</ul>",
|
||||
Image = new ImageModel { Src = "https://dummyimage.com/600x400/343a40/6c757d", Alt = "..." },
|
||||
PrimaryLink = new MenuItemModel("Get Started", "#features"),
|
||||
SecondaryLink = new MenuItemModel("Learn More", "#!")
|
||||
},
|
||||
FeaturesSection = new {
|
||||
|
||||
FeaturesSection = new FeaturesSectionModel {
|
||||
Title = "To help you get started, we have also set up:",
|
||||
Items = new[] {
|
||||
new {
|
||||
Icon = "navigation",
|
||||
Title = "Client-side navigation",
|
||||
Text = "For example, click <em>Counter</em> then <em>Back</em> to return here."
|
||||
},
|
||||
new {
|
||||
Icon = "server",
|
||||
Title = "Development server integration",
|
||||
Text = "In development mode, the development server from <code>create-react-app</code> runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file."
|
||||
},
|
||||
new {
|
||||
Icon = "terminal",
|
||||
Title = "Efficient production builds",
|
||||
Text = "In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration produces minified, efficiently bundled JavaScript files."
|
||||
Items = new List<FeatureModel> {
|
||||
new FeatureModel {
|
||||
Icon = "navigation",
|
||||
Title = "Client-side navigation",
|
||||
Text = "For example, click <em>Counter</em> then <em>Back</em> to return here."
|
||||
},
|
||||
new FeatureModel {
|
||||
Icon = "server",
|
||||
Title = "Development server integration",
|
||||
Text = "In development mode, the development server from <code>create-react-app</code> runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file."
|
||||
},
|
||||
new FeatureModel {
|
||||
Icon = "terminal",
|
||||
Title = "Efficient production builds",
|
||||
Text = "In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration produces minified, efficiently bundled JavaScript files."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
TestimonialsSection = new {
|
||||
Items = new[] {
|
||||
new {
|
||||
TestimonialsSection = new TestimonialsSectionModel {
|
||||
Items = new List<TestimonialModel> {
|
||||
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>.",
|
||||
Author = new AuthorModel {
|
||||
Reviewer = new ReviewerModel {
|
||||
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",
|
||||
Items = blogItems
|
||||
},
|
||||
CallToActionSection = new {
|
||||
|
||||
CallToActionSection = new CallToActionSectionModel {
|
||||
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."
|
||||
PrivacyDisclaimer = "We care about privacy, and will never share your data.",
|
||||
Email = new FormItemModel {
|
||||
PlaceHolder = "Email address...",
|
||||
Title = "Sign up"
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
pages.Add(new {
|
||||
Id = "ShopCatalog",
|
||||
TitleSection = new {
|
||||
var shopCatalogPage = new ShopCatalogPageModel {
|
||||
TitleSection = new TitleSectionModel {
|
||||
Title = "Shop in style",
|
||||
Text = "With this shop hompeage template"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
pages.Add(new {
|
||||
Id = "BlogCatalog",
|
||||
TitleSection = new {
|
||||
var blogCatalogPage = new BlogCatalogPageModel {
|
||||
TitleSection = new TitleSectionModel {
|
||||
Title = "Welcome to Blog Home!",
|
||||
Text = "A Bootstrap 5 starter layout for your next blog homepage"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return Ok(new {
|
||||
return Ok(new GetStaticContentResponseModel {
|
||||
SiteName = "MAKS-IT",
|
||||
|
||||
Routes = routes,
|
||||
@ -209,7 +215,9 @@ public class StaticContentController : ControllerBase {
|
||||
|
||||
TopMenu = topMenu,
|
||||
SideMenu = sideMenu,
|
||||
Pages = pages
|
||||
HomePage = homePage,
|
||||
ShopCatalog = shopCatalogPage,
|
||||
BlogCatalog = blogCatalogPage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +1,9 @@
|
||||
using Core.Abstractions.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using WeatherForecast.Models;
|
||||
using WeatherForecast.Models.Responses;
|
||||
|
||||
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]
|
||||
[Route("[controller]")]
|
||||
public class WeatherForecastController : ControllerBase
|
||||
@ -35,9 +21,9 @@ public class WeatherForecastController : ControllerBase
|
||||
}
|
||||
|
||||
[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),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
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 Guid Id { get; set; }
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
namespace WeatherForecast.Models {
|
||||
public class AuthorModel {
|
||||
public Guid Id { get; set; }
|
||||
public ImageModel Image { get; set; }
|
||||
using WeatherForecast.Models.Abstractions;
|
||||
|
||||
namespace WeatherForecast.Models {
|
||||
public class AuthorModel : PersonModel {
|
||||
public string NickName { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
namespace WeatherForecast.Models {
|
||||
using WeatherForecast.Models.Abstractions;
|
||||
|
||||
namespace WeatherForecast.Models {
|
||||
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 List<ImageModel> Images { 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>
|
||||
<RootNamespace>WeatherForecast</RootNamespace>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<UserSecretsId>2ea970dd-e71a-4c8e-9ff6-2d1d3123d4df</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -21,4 +18,8 @@
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Models\Requests\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user