(feat): redux loader

This commit is contained in:
Maksym Sadovnychyy 2022-05-30 22:04:33 +02:00
parent 9643070e86
commit 268c1d0060
99 changed files with 4245 additions and 521 deletions

View File

@ -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} /> : ''}
</>
}

View 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
}

View 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.

View File

@ -0,0 +1,3 @@
@function delay($interval, $count, $index) {
@return ($index * $interval) - ($interval * $count);
}

View 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;
}

View File

@ -0,0 +1,6 @@
$primary-color: #fff !default;
$ball-size: 15px !default;
$margin: 2px !default;
$line-height: 35px !default;
$line-width: 4px !default;

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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%;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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; }

View 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>

View 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';
});

View 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;
}
}

View 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';

View 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
}

View 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
}

View File

@ -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
}

View File

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

View File

@ -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
}

View File

@ -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
}

View 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
}

View File

@ -1,7 +1,9 @@
import { dateFormat } from './dateFormat'
import { findRoutes } from './findRoutes'
import { getKeyValue } from './getKeyValue'
export {
getKeyValue,
dateFormat
dateFormat,
findRoutes
}

View File

@ -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 &copy; {siteName} {(new Date).getFullYear()}</p></Container>*/}
<Container fluid><p className="m-0 text-center text-white">Copyright &copy; {content?.siteName} {(new Date).getFullYear()}</p></Container>
</footer>
}

View File

@ -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>
}

View 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 {
}

View File

@ -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
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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} &middot; {item.readTime}</div>
<div className="text-muted">{dateFormat(item.created)} &middot; {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} />
</>
}

View File

@ -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} /> : ''}
</>
}

View File

@ -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 = () => {

View File

@ -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

View 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
}

View File

@ -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
}
}

View 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
}

View 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
}

View File

@ -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

View File

@ -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 {

View File

@ -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,

View File

@ -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
});
}
}

View File

@ -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)]

View File

@ -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"]

View File

@ -0,0 +1,4 @@

namespace WeatherForecast.Models.Abstractions {
public abstract class PageModel { }
}

View File

@ -0,0 +1,6 @@
namespace WeatherForecast.Models.Abstractions {
public abstract class PageSectionModel {
public string? Title { get; set; }
public string? Text { get; set; }
}
}

View File

@ -0,0 +1,6 @@
namespace WeatherForecast.Models.Abstractions {
public abstract class PersonModel {
public Guid Id { get; set; }
public ImageModel Image { get; set; }
}
}

View File

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

View File

@ -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; }
}
}

View File

@ -1,4 +1,6 @@
namespace WeatherForecast.Models {
using WeatherForecast.Models.Abstractions;
namespace WeatherForecast.Models {
public class BlogItemModel : PostItemModel {

View 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; }
}
}

View File

@ -0,0 +1,6 @@
namespace WeatherForecast.Models {
public class FormItemModel {
public string? Title { get; set; }
public string? PlaceHolder { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,7 @@
using WeatherForecast.Models.Abstractions;
namespace WeatherForecast.Models.PageSections {
public class FeaturedBologsSectionModel : PageSectionModel {
public List<BlogItemModel> Items { get; set; }
}
}

View File

@ -0,0 +1,7 @@
using WeatherForecast.Models.Abstractions;
namespace WeatherForecast.Models.PageSections {
public class FeaturesSectionModel : PageSectionModel {
public List<FeatureModel> Items { get; set; }
}
}

View File

@ -0,0 +1,7 @@
using WeatherForecast.Models.Abstractions;
namespace WeatherForecast.Models.PageSections {
public class TestimonialsSectionModel : PageSectionModel {
public List<TestimonialModel> Items { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View 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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View 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; }
}
}

View File

@ -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; }

View File

@ -0,0 +1,6 @@
namespace WeatherForecast.Models {
public class TestimonialModel {
public string Text { get; set; }
public ReviewerModel Reviewer { get; set; }
}
}

View File

@ -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>