diff --git a/clientapp/src/App.tsx b/clientapp/src/App.tsx index 4abe20a..1b8de5e 100644 --- a/clientapp/src/App.tsx +++ b/clientapp/src/App.tsx @@ -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 <> - {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) : ''} + + {loader ? : ''} } diff --git a/clientapp/src/components/Loader/index.tsx b/clientapp/src/components/Loader/index.tsx new file mode 100644 index 0000000..516dedb --- /dev/null +++ b/clientapp/src/components/Loader/index.tsx @@ -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 = ({ + visible = false, + loaderType = 'ballScaleMultiple', + color = '#000', + background = '#fff' +}) => { + + interface loaderItem { + loader: ReactNode, + tooltip: ReactNode + } + + interface loadersDictionary { + [key: string]: loaderItem + } + + + const loaders: loadersDictionary = { + ballPulse: { + loader:
+ {[...Array(3)].map((item, index) =>
)} +
, + tooltip:

ball-pulse

+ }, + + ballGridPulse: { + loader:
+ {[...Array(9)].map((item, index) =>
)} +
, + tooltip:

ball-grid-pulse

+ }, + + ballClipRotate: { + loader:
+
+
, + tooltip:

ball-clip-rotate

+ }, + + ballClipRotatePulse: { + loader:
+ {[...Array(2)].map((item, index) =>
)} +
, + tooltip:

ball-clip-rotate-pulse

+ }, + + squareSpin: { + loader:
+
+
, + tooltip:

square-spin

+ }, + + ballClipRotateMultiple: { + loader:
+ {[...Array(2)].map((item, index) =>
)} +
, + tooltip:

ball-clip-rotate-multiple

+ }, + + ballPulseRise: { + loader:
+ {[...Array(5)].map((item, index) =>
)} +
, + tooltip:

ball-pulse-rise

+ }, + + ballRotate: { + loader:
+
+
, + tooltip:

ball-rotate

+ }, + + cubeTransion: { + loader:
+ {[...Array(2)].map((item, index) =>
)} +
, + tooltip:

cube-transition

+ }, + + ballZigZag: { + loader:
+ {[...Array(2)].map((item, index) =>
)} +
, + tooltip:

ball-zig-zag

+ }, + + ballZigZagDeflect: { + loader:
+ {[...Array(2)].map((item, index) =>
)} +
, + tooltip:

ball-zig-zag-deflect

+ }, + + ballTrianglePath: { + loader:
+ {[...Array(3)].map((item, index) =>
)} +
, + tooltip:

ball-triangle-path

+ }, + + ballScale: { + loader:
+
+
, + tooltip:

ball-scale

+ }, + + lineScale: { + loader:
+ {[...Array(5)].map((item, index) =>
)} +
, + tooltip:

line-scale

+ }, + + lineScaleParty: { + loader:
+ {[...Array(4)].map((item, index) =>
)} +
, + tooltip:

line-scale-party

+ }, + + ballScaleMultiple: { + loader:
+ {[...Array(3)].map((item, index) =>
)} +
, + tooltip:

ball-scale-multiple

+ }, + + ballPulseSync: { + loader:
+ {[...Array(3)].map((item, index) =>
)} +
, + tooltip:

ball-pulse-sync

+ }, + + ballBeat: { + loader:
+ {[...Array(3)].map((item, index) =>
)} +
, + tooltip:

ball-beat

+ }, + + lineScalePulseOut: { + loader:
+ {[...Array(5)].map((item, index) =>
)} +
, + tooltip:

line-scale-pulse-out

+ }, + + lineScalePulseOutRapid: { + loader:
+ {[...Array(5)].map((item, index) =>
)} +
, + tooltip:

line-scale-pulse-out-rapid

+ }, + + ballScaleRipple: { + loader:
+
+
, + tooltip:

ball-scale-ripple

+ }, + + ballScaleRippleMultiple: { + loader:
+ {[...Array(3)].map((item, index) =>
)} +
, + tooltip:

ball-scale-ripple-multiple

+ }, + + ballSpinFadeLoader: { + loader:
+ {[...Array(8)].map((item, index) =>
)} +
, + tooltip:

ball-spin-fade-loader

+ }, + + lineSpinFadeLoader: { + loader:
+ {[...Array(8)].map((item, index) =>
)} +
, + tooltip:

line-spin-fade-loader

+ }, + + triangleSkewSpin: { + loader:
+
+
, + tooltip:

triangle-skew-spin

+ }, + + pacman: { + loader:
+ {[...Array(5)].map((item, index) =>
)} +
, + tooltip:

pacman

+ }, + + semiCircleSpin: { + loader:
+
+
, + tooltip:

semi-circle-spin

+ }, + + ballGridBeat: { + loader:
+ {[...Array(9)].map((item, index) =>
)} +
, + tooltip:

ball-grid-beat

+ }, + + ballScaleRandom: { + loader:
+ {[...Array(3)].map((item, index) =>
)} +
, + tooltip:

ball-scale-random

+ } + } + + 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
+
+ {loaders[loaderType].loader} +
+
+} + +export { + Loader +} \ No newline at end of file diff --git a/clientapp/src/components/Loader/scss/README.md b/clientapp/src/components/Loader/scss/README.md new file mode 100644 index 0000000..7a83ed9 --- /dev/null +++ b/clientapp/src/components/Loader/scss/README.md @@ -0,0 +1,126 @@ +

Loaders.css

+ +

+ + +

+ +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. `
`) +- Insert the appropriate number of `
`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. `
`) +- `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. \ No newline at end of file diff --git a/clientapp/src/components/Loader/scss/_functions.scss b/clientapp/src/components/Loader/scss/_functions.scss new file mode 100644 index 0000000..f417aee --- /dev/null +++ b/clientapp/src/components/Loader/scss/_functions.scss @@ -0,0 +1,3 @@ +@function delay($interval, $count, $index) { + @return ($index * $interval) - ($interval * $count); +} diff --git a/clientapp/src/components/Loader/scss/_mixins.scss b/clientapp/src/components/Loader/scss/_mixins.scss new file mode 100644 index 0000000..a204fe6 --- /dev/null +++ b/clientapp/src/components/Loader/scss/_mixins.scss @@ -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; +} diff --git a/clientapp/src/components/Loader/scss/_variables.scss b/clientapp/src/components/Loader/scss/_variables.scss new file mode 100644 index 0000000..61b0022 --- /dev/null +++ b/clientapp/src/components/Loader/scss/_variables.scss @@ -0,0 +1,6 @@ +$primary-color: #fff !default; +$ball-size: 15px !default; +$margin: 2px !default; +$line-height: 35px !default; +$line-width: 4px !default; + diff --git a/clientapp/src/components/Loader/scss/animations/ball-beat.scss b/clientapp/src/components/Loader/scss/animations/ball-beat.scss new file mode 100644 index 0000000..b32563b --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-beat.scss @@ -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; + } + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-clip-rotate-multiple.scss b/clientapp/src/components/Loader/scss/animations/ball-clip-rotate-multiple.scss new file mode 100644 index 0000000..aa6ad7f --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-clip-rotate-multiple.scss @@ -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; + } + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-clip-rotate-pulse.scss b/clientapp/src/components/Loader/scss/animations/ball-clip-rotate-pulse.scss new file mode 100644 index 0000000..538c53d --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-clip-rotate-pulse.scss @@ -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; + } + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-clip-rotate.scss b/clientapp/src/components/Loader/scss/animations/ball-clip-rotate.scss new file mode 100644 index 0000000..ce40202 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-clip-rotate.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-grid-beat.scss b/clientapp/src/components/Loader/scss/animations/ball-grid-beat.scss new file mode 100644 index 0000000..88885a6 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-grid-beat.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-grid-pulse.scss b/clientapp/src/components/Loader/scss/animations/ball-grid-pulse.scss new file mode 100644 index 0000000..8d2b9b4 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-grid-pulse.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-pulse-rise.scss b/clientapp/src/components/Loader/scss/animations/ball-pulse-rise.scss new file mode 100644 index 0000000..808a4dd --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-pulse-rise.scss @@ -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; + } + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-pulse-round.scss b/clientapp/src/components/Loader/scss/animations/ball-pulse-round.scss new file mode 100644 index 0000000..56a65d1 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-pulse-round.scss @@ -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; + } +} + diff --git a/clientapp/src/components/Loader/scss/animations/ball-pulse-sync.scss b/clientapp/src/components/Loader/scss/animations/ball-pulse-sync.scss new file mode 100644 index 0000000..c7c1294 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-pulse-sync.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-pulse.scss b/clientapp/src/components/Loader/scss/animations/ball-pulse.scss new file mode 100644 index 0000000..16c23c3 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-pulse.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-rotate.scss b/clientapp/src/components/Loader/scss/animations/ball-rotate.scss new file mode 100644 index 0000000..ce2b41a --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-rotate.scss @@ -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; + } + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-scale-multiple.scss b/clientapp/src/components/Loader/scss/animations/ball-scale-multiple.scss new file mode 100644 index 0000000..035e515 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-scale-multiple.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-scale-random.scss b/clientapp/src/components/Loader/scss/animations/ball-scale-random.scss new file mode 100644 index 0000000..dba15ca --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-scale-random.scss @@ -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; + } + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-scale-ripple-multiple.scss b/clientapp/src/components/Loader/scss/animations/ball-scale-ripple-multiple.scss new file mode 100644 index 0000000..ccaa8e4 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-scale-ripple-multiple.scss @@ -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); + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-scale-ripple.scss b/clientapp/src/components/Loader/scss/animations/ball-scale-ripple.scss new file mode 100644 index 0000000..804b58b --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-scale-ripple.scss @@ -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); + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-scale.scss b/clientapp/src/components/Loader/scss/animations/ball-scale.scss new file mode 100644 index 0000000..5617139 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-scale.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-spin-fade-loader.scss b/clientapp/src/components/Loader/scss/animations/ball-spin-fade-loader.scss new file mode 100644 index 0000000..d0c2638 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-spin-fade-loader.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-spin-loader.scss b/clientapp/src/components/Loader/scss/animations/ball-spin-loader.scss new file mode 100644 index 0000000..4022908 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-spin-loader.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-triangle-path.scss b/clientapp/src/components/Loader/scss/animations/ball-triangle-path.scss new file mode 100644 index 0000000..d27e8b5 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-triangle-path.scss @@ -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; + } + } +} diff --git a/clientapp/src/components/Loader/scss/animations/ball-zig-zag-deflect.scss b/clientapp/src/components/Loader/scss/animations/ball-zig-zag-deflect.scss new file mode 100644 index 0000000..7959000 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-zig-zag-deflect.scss @@ -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; + } + } +} + diff --git a/clientapp/src/components/Loader/scss/animations/ball-zig-zag.scss b/clientapp/src/components/Loader/scss/animations/ball-zig-zag.scss new file mode 100644 index 0000000..4866f48 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/ball-zig-zag.scss @@ -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; + } + } +} diff --git a/clientapp/src/components/Loader/scss/animations/cube-transition.scss b/clientapp/src/components/Loader/scss/animations/cube-transition.scss new file mode 100644 index 0000000..2c29170 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/cube-transition.scss @@ -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 + } + } +} diff --git a/clientapp/src/components/Loader/scss/animations/line-scale-pulse-out-rapid.scss b/clientapp/src/components/Loader/scss/animations/line-scale-pulse-out-rapid.scss new file mode 100644 index 0000000..a8fe390 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/line-scale-pulse-out-rapid.scss @@ -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; + } + } +} diff --git a/clientapp/src/components/Loader/scss/animations/line-scale-pulse-out.scss b/clientapp/src/components/Loader/scss/animations/line-scale-pulse-out.scss new file mode 100644 index 0000000..6662867 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/line-scale-pulse-out.scss @@ -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; + } + + } +} diff --git a/clientapp/src/components/Loader/scss/animations/line-scale-random.scss b/clientapp/src/components/Loader/scss/animations/line-scale-random.scss new file mode 100644 index 0000000..0706471 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/line-scale-random.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/line-scale.scss b/clientapp/src/components/Loader/scss/animations/line-scale.scss new file mode 100644 index 0000000..9a0a5b1 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/line-scale.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/line-spin-fade-loader.scss b/clientapp/src/components/Loader/scss/animations/line-spin-fade-loader.scss new file mode 100644 index 0000000..ed1e9cf --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/line-spin-fade-loader.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/pacman.scss b/clientapp/src/components/Loader/scss/animations/pacman.scss new file mode 100644 index 0000000..652a42f --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/pacman.scss @@ -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; + } +} \ No newline at end of file diff --git a/clientapp/src/components/Loader/scss/animations/semi-circle-spin.scss b/clientapp/src/components/Loader/scss/animations/semi-circle-spin.scss new file mode 100644 index 0000000..91ae1e0 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/semi-circle-spin.scss @@ -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%; + } +} \ No newline at end of file diff --git a/clientapp/src/components/Loader/scss/animations/square-spin.scss b/clientapp/src/components/Loader/scss/animations/square-spin.scss new file mode 100644 index 0000000..19698b1 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/square-spin.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/animations/triangle-skew-spin.scss b/clientapp/src/components/Loader/scss/animations/triangle-skew-spin.scss new file mode 100644 index 0000000..bc55313 --- /dev/null +++ b/clientapp/src/components/Loader/scss/animations/triangle-skew-spin.scss @@ -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; + } +} diff --git a/clientapp/src/components/Loader/scss/demo/demo.css b/clientapp/src/components/Loader/scss/demo/demo.css new file mode 100644 index 0000000..86e9fc8 --- /dev/null +++ b/clientapp/src/components/Loader/scss/demo/demo.css @@ -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; } diff --git a/clientapp/src/components/Loader/scss/demo/demo.html b/clientapp/src/components/Loader/scss/demo/demo.html new file mode 100644 index 0000000..36563c0 --- /dev/null +++ b/clientapp/src/components/Loader/scss/demo/demo.html @@ -0,0 +1,270 @@ + + + + + + + +
+
+
+

Loaders.css

+

Delightful and performance-focused pure css loading animations.

+
+ +
+
+
+
+
+
+
+
+

ball-pulse

+
+
+
+
+
+
+
+
+
+
+
+
+
+

ball-grid-pulse

+
+
+
+
+
+

ball-clip-rotate

+
+
+
+
+
+
+

ball-clip-rotate-pulse

+
+
+
+
+
+

square-spin

+
+
+
+
+
+
+

ball-clip-rotate-multiple

+
+
+
+
+
+
+
+
+
+

ball-pulse-rise

+
+
+
+
+
+

ball-rotate

+
+
+
+
+
+
+

cube-transition

+
+
+
+
+
+
+

ball-zig-zag

+
+
+
+
+
+
+

ball-zig-zag-deflect

+
+
+
+
+
+
+
+

ball-triangle-path

+
+
+
+
+
+

ball-scale

+
+
+
+
+
+
+
+
+
+

line-scale

+
+
+
+
+
+
+
+
+

line-scale-party

+
+
+
+
+
+
+
+

ball-scale-multiple

+
+
+
+
+
+
+
+

ball-pulse-sync

+
+
+
+
+
+
+
+

ball-beat

+
+
+
+
+
+
+
+
+
+

line-scale-pulse-out

+
+
+
+
+
+
+
+
+
+

line-scale-pulse-out-rapid

+
+
+
+
+
+

ball-scale-ripple

+
+
+
+
+
+
+
+

ball-scale-ripple-multiple

+
+
+
+
+
+
+
+
+
+
+
+
+

ball-spin-fade-loader

+
+
+
+
+
+
+
+
+
+
+
+
+

line-spin-fade-loader

+
+
+
+
+
+

triangle-skew-spin

+
+
+
+
+
+
+
+
+
+

pacman

+
+
+
+
+
+

semi-circle-spin

+
+
+
+
+
+
+
+
+
+
+
+
+
+

ball-grid-beat

+
+
+
+
+
+
+
+

ball-scale-random

+
+
+
+ + \ No newline at end of file diff --git a/clientapp/src/components/Loader/scss/demo/src/demo.jade b/clientapp/src/components/Loader/scss/demo/src/demo.jade new file mode 100644 index 0000000..39a0279 --- /dev/null +++ b/clientapp/src/components/Loader/scss/demo/src/demo.jade @@ -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'; + }); diff --git a/clientapp/src/components/Loader/scss/demo/src/demo.scss b/clientapp/src/components/Loader/scss/demo/src/demo.scss new file mode 100644 index 0000000..7236592 --- /dev/null +++ b/clientapp/src/components/Loader/scss/demo/src/demo.scss @@ -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; + } +} + diff --git a/clientapp/src/components/Loader/scss/loaders.scss b/clientapp/src/components/Loader/scss/loaders.scss new file mode 100644 index 0000000..e152c30 --- /dev/null +++ b/clientapp/src/components/Loader/scss/loaders.scss @@ -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'; diff --git a/clientapp/src/components/Pagination/index.tsx b/clientapp/src/components/Pagination/index.tsx new file mode 100644 index 0000000..2ad8f2e --- /dev/null +++ b/clientapp/src/components/Pagination/index.tsx @@ -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 = ({ + maxVisiblePages = 5, + totalPages = 1, + currentPage = 1, + onClick +}) => { + + // << & >> buttons + let firstButton = <> + if (currentPage > 1) { + firstButton = { onClick(1) }} /> + } + + let lastButton = <> + if (currentPage < totalPages) { + lastButton = { onClick(totalPages) }} /> + } + + // < & > buttons + let prevButton = <> + if (currentPage > 1) { + prevButton = { onClick(currentPage - 1) }} /> + } + + let nextButton = <> + if (currentPage < totalPages) { + nextButton = { 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 = { onClick(chunks[chunk.index - 1][0]) }}>{'...'} + } + + if (chunk.index < chunks.length - 1) { + nextChunk = { onClick(chunks[chunk.index + 1][0]) }}>{'...'} + } + } + + // numbered pagination buttons + const pageButtons = [] + for (let i = 0; i < chunk.items.length; i++) { + if (chunk.items[i] === currentPage) { + pageButtons.push({chunk.items[i]}) + } else { + pageButtons.push( { onClick(chunk.items[i]) }}>{chunk.items[i]}) + } + } + + return +} + +interface SSRPaginationProps { + maxVisiblePages?: number, + totalPages: number, + currentPage: number, + linksPath?: string +} + +const SSRPagination: FC = ({ + maxVisiblePages = 5, + totalPages = 1, + currentPage = 1, + linksPath +}) => { + + + if (!linksPath) { + return ( +
+ Server Side Prerendering Pagination disabled (Missing Link Path) +
+ ) + } + + // << & >> buttons + let firstButton = <> + if (currentPage > 1) { + firstButton = + } + + let lastButton = <> + if (currentPage < totalPages) { + lastButton = + } + + // < & > buttons + let prevButton = <> + if (currentPage > 1) { + prevButton = + } + + let nextButton = <> + if (currentPage < totalPages) { + nextButton = + } + + const chunks = splitInChunks(intToArray(totalPages), maxVisiblePages) + const chunk = findChunk(chunks, currentPage) + + // ... & ... buttons + let prevChunk = <> + let nextChunk = <> + if (totalPages > maxVisiblePages) { + if (chunk.index > 0) { + prevChunk = {'...'} + } + + if (chunk.index < chunks.length - 1) { + nextChunk = {'...'} + } + } + + // numbered pagination buttons + const pageButtons = [] + for (let i = 0; i < chunk.items.length; i++) { + if (chunk.items[i] === currentPage) { + pageButtons.push({chunk.items[i]}) + } else { + pageButtons.push({chunk.items[i]}) + } + } + + return +} + +export { + Pagination, + SSRPagination +} \ No newline at end of file diff --git a/clientapp/src/components/Pagination/utils.ts b/clientapp/src/components/Pagination/utils.ts new file mode 100644 index 0000000..b32d388 --- /dev/null +++ b/clientapp/src/components/Pagination/utils.ts @@ -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 +} \ No newline at end of file diff --git a/clientapp/src/controllers/blogCatalog.ts b/clientapp/src/controllers/blogCatalog.ts deleted file mode 100644 index e9d0131..0000000 --- a/clientapp/src/controllers/blogCatalog.ts +++ /dev/null @@ -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 => await Get>(apiUrl, props) - -export { - GetBlogCatalog -} \ No newline at end of file diff --git a/clientapp/src/controllers/blogItem.ts b/clientapp/src/controllers/blogItem.ts deleted file mode 100644 index 0148c0c..0000000 --- a/clientapp/src/controllers/blogItem.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { IBlogItemModel, ICategoryModel } from "../models" - -const apiUrl = 'https://localhost:59018/api/Blog' \ No newline at end of file diff --git a/clientapp/src/controllers/shopCatalog.ts b/clientapp/src/controllers/shopCatalog.ts deleted file mode 100644 index 8bcaaf6..0000000 --- a/clientapp/src/controllers/shopCatalog.ts +++ /dev/null @@ -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 => await Get>(apiUrl, props) - -export { - GetShopCatalog -} \ No newline at end of file diff --git a/clientapp/src/controllers/staticContent.ts b/clientapp/src/controllers/staticContent.ts deleted file mode 100644 index 325298e..0000000 --- a/clientapp/src/controllers/staticContent.ts +++ /dev/null @@ -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 => await Get>(apiUrl, props) - -export { - GetStaticContent -} \ No newline at end of file diff --git a/clientapp/src/functions/findRoutes.ts b/clientapp/src/functions/findRoutes.ts new file mode 100644 index 0000000..7ef704d --- /dev/null +++ b/clientapp/src/functions/findRoutes.ts @@ -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 +} \ No newline at end of file diff --git a/clientapp/src/functions/index.ts b/clientapp/src/functions/index.ts index fda4aa0..e636104 100644 --- a/clientapp/src/functions/index.ts +++ b/clientapp/src/functions/index.ts @@ -1,7 +1,9 @@ import { dateFormat } from './dateFormat' +import { findRoutes } from './findRoutes' import { getKeyValue } from './getKeyValue' export { getKeyValue, - dateFormat + dateFormat, + findRoutes } \ No newline at end of file diff --git a/clientapp/src/layouts/public/Footer/index.tsx b/clientapp/src/layouts/public/Footer/index.tsx index edff5da..17e9146 100644 --- a/clientapp/src/layouts/public/Footer/index.tsx +++ b/clientapp/src/layouts/public/Footer/index.tsx @@ -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
- {/*

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

*/} +

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

} diff --git a/clientapp/src/layouts/public/NavMenu/index.tsx b/clientapp/src/layouts/public/NavMenu/index.tsx index 9b73207..29f6839 100644 --- a/clientapp/src/layouts/public/NavMenu/index.tsx +++ b/clientapp/src/layouts/public/NavMenu/index.tsx @@ -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
- {/** - {siteName} + {content?.siteName}
    - {topMenu.map((item: IMenuItemModel, index: number) => { + {content?.topMenu ? content.topMenu.map((item: MenuItemModel, index: number) => { return {item.icon ? : ''} {item.title} - })} + }) : ''}
@@ -47,7 +44,6 @@ const NavMenu : FC = () => {
- */}
} diff --git a/clientapp/src/models/abstractions.ts b/clientapp/src/models/abstractions.ts new file mode 100644 index 0000000..e576289 --- /dev/null +++ b/clientapp/src/models/abstractions.ts @@ -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 { + +} diff --git a/clientapp/src/models/index.ts b/clientapp/src/models/index.ts index 003518e..efb9bf8 100644 --- a/clientapp/src/models/index.ts +++ b/clientapp/src/models/index.ts @@ -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 { + 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 -} \ No newline at end of file diff --git a/clientapp/src/models/pageSections.ts b/clientapp/src/models/pageSections.ts new file mode 100644 index 0000000..b12aef8 --- /dev/null +++ b/clientapp/src/models/pageSections.ts @@ -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 +} diff --git a/clientapp/src/models/pages.ts b/clientapp/src/models/pages.ts new file mode 100644 index 0000000..6e3224f --- /dev/null +++ b/clientapp/src/models/pages.ts @@ -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 +} diff --git a/clientapp/src/models/requests.ts b/clientapp/src/models/requests.ts new file mode 100644 index 0000000..be97b32 --- /dev/null +++ b/clientapp/src/models/requests.ts @@ -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 +} diff --git a/clientapp/src/models/responses.ts b/clientapp/src/models/responses.ts new file mode 100644 index 0000000..cf8556c --- /dev/null +++ b/clientapp/src/models/responses.ts @@ -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 +} + +export interface GetShopCatalogResponseModel extends ResponseModel { + shopItemsPagination: PaginationModel +} + +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 +} \ No newline at end of file diff --git a/clientapp/src/pages/Blog/Catalog/index.tsx b/clientapp/src/pages/Blog/Catalog/index.tsx index bca91aa..c5824e3 100644 --- a/clientapp/src/pages/Blog/Catalog/index.tsx +++ b/clientapp/src/pages/Blog/Catalog/index.tsx @@ -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 = (props) => { + const { title, text } = props + return
+ +
+

{title ? title : ''}

+

{text ? text : ''}

+
+
+
+} -const FeaturedBlog: FC = (props) => { +const FeaturedBlog: FC = (props) => { const { id, slug, badge, image, title, shortText, author, created, readTime, likes, tags } = props return
{badge}
- +
{title}

@@ -38,8 +54,15 @@ const FeaturedBlog: FC = (props) => { } -const BlogItemsPagination: FC = (props) => { - const { items, currentPage, totalPages } = props +interface BlogItemsPaginationModel extends PaginationModel { + path: string +} + +const BlogItemsPagination: FC = (props) => { + const { items, currentPage, totalPages, path } = props + + const dispatch = useDispatch() + const navigate = useNavigate() return <> {items.map((item, index) => @@ -48,61 +71,66 @@ const BlogItemsPagination: FC = (props) => { -
{item.created}
+
{dateFormat(item.created)}

{item.title}

{item.shortText}

- Read more → + Read more →
)} - + { + dispatch(blogCatalogActionCreators.requestBlogCatalog({ + currentPage: nextPage + "" + })) + + navigate(`${path}/${nextPage}`) + } + }} /> } const BlogCatalog = () => { - const [state, setState] = useState() + 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 <> -
- -
-

Welcome to Blog Home!

-

A Bootstrap 5 starter layout for your next blog homepage

-
-
-
+ useEffect(() => { + blogCatalog?.isLoading + ? dispatch(loaderActionCreators.show()) + : setTimeout(() => { + dispatch(loaderActionCreators.hide()) + }, 1000) + }, [blogCatalog?.isLoading]) + return <> + - {state?.featuredBlog ? : ''} + {blogCatalog?.featuredBlog ? : ''} - {state?.blogItemsPagination ? : '' } + {blogCatalog?.blogItemsPagination ? : '' } - {state?.categories ? : '' } diff --git a/clientapp/src/pages/Blog/SideWidgets/index.tsx b/clientapp/src/pages/Blog/SideWidgets/index.tsx index 8b1548a..8af7df7 100644 --- a/clientapp/src/pages/Blog/SideWidgets/index.tsx +++ b/clientapp/src/pages/Blog/SideWidgets/index.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Card, CardBody, CardHeader, Col, Row } from 'reactstrap' -import { ICategoryModel } from '../../../models' +import { CategoryModel } from '../../../models' const Search = () => { return @@ -14,8 +14,8 @@ const Search = () => { } -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 diff --git a/clientapp/src/pages/FetchData.tsx b/clientapp/src/pages/FetchData.tsx index f230a5e..c8f0e2a 100644 --- a/clientapp/src/pages/FetchData.tsx +++ b/clientapp/src/pages/FetchData.tsx @@ -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 = () => { {forecasts.map((forecast: WeatherForecast) => - {forecast.date} + {dateFormat(forecast.date)} {forecast.temperatureC} {forecast.temperatureF} {forecast.summary} diff --git a/clientapp/src/pages/Home/index.tsx b/clientapp/src/pages/Home/index.tsx index 8f13ab4..17c1e24 100644 --- a/clientapp/src/pages/Home/index.tsx +++ b/clientapp/src/pages/Home/index.tsx @@ -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 = (props) => { +const TitleSection : FC = (props) => { const { title, text } = props return
@@ -59,7 +30,7 @@ const TitleSection : FC = (props) => {

{title}

- +
@@ -77,46 +48,45 @@ const TitleSection : FC = (props) => { -const FeaturesSection: FC = (props) => { +const FeaturesSection: FC = (props) => { const { title, items } = props return
-

{title}

+

{title ? title : ''}

- {items.map((item, index) => + {items ? items.map((item, index) =>

{item.title}

- )} + ) : ''}
- } +const TestimonialsSection: FC = (props) => { + const item = props?.items ? props?.items.shift() : undefined - -const TestimonialsSection: FC = (props) => { - const { text, image } = props + if(!item) return <> return
-
+
- -
Tom Ato/CEO, Pomodoro + +
{item.reviewer.fullName}/{item.reviewer.position}
@@ -126,12 +96,7 @@ const TestimonialsSection: FC = (props) => {
} - - - - - -const FromOurBlogSection: FC = (props) => { +const FromOurBlogSection: FC = (props) => { const { title, text, items } = props return
@@ -139,13 +104,13 @@ const FromOurBlogSection: FC = (props) => {
-

{title}

-

+

{title ? title : ''}

+

- {items.map((item, index) => + {items ? items.map((item, index) => @@ -153,7 +118,7 @@ const FromOurBlogSection: FC = (props) => {
{item.title}
-

+

@@ -161,22 +126,21 @@ const FromOurBlogSection: FC = (props) => {
{item.author.nickName}
-
{item.created} · {item.readTime}
+
{dateFormat(item.created)} · {item.readTime}
- )} + ) : ''} } - - -const CallToActionSection: FC = (props) => { - const { title, text, privacyDisclaimer } = props +const CallToActionSection: FC = (props) => { + const { title, text, privacyDisclaimer, email } = props + return
- - + +
{privacyDisclaimer}
@@ -199,16 +163,27 @@ const CallToActionSection: FC = (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 ? : '' } - { page?.featuresSection ? : '' } - { page?.testimonialsSection ? : '' } - { page?.featuredBlogsSection ? : '' } - { page?.callToActionSection ? :'' } + + + + + } diff --git a/clientapp/src/pages/Shop/Catalog/index.tsx b/clientapp/src/pages/Shop/Catalog/index.tsx index 51e1937..8567c4d 100644 --- a/clientapp/src/pages/Shop/Catalog/index.tsx +++ b/clientapp/src/pages/Shop/Catalog/index.tsx @@ -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 = (props) => { + const { title, text } = props -const ShopItemsPagination: FC = (props) => { - const { items, currentPage, totalPages } = props + return
+ + +
+

{title ? title : ''}

+

{text ? text : ''}

+
+
+
+} + +interface ShopItemsPaginationModel extends PaginationModel { + path: string +} + +const ShopItemsPagination: FC = (props) => { + const { items, currentPage, totalPages, path } = props + + const dispatch = useDispatch() + const navigate = useNavigate() return
@@ -18,7 +53,7 @@ const ShopItemsPagination: FC = (props) => {
{item.badge}
- + @@ -43,82 +78,52 @@ const ShopItemsPagination: FC = (props) => { )} + + + + { + dispatch(shopCatalogActionCreators.requestShopCatalog({ + currentPage: nextPage + "" + })) + + navigate(`${path}/${nextPage}`) + } + }} />
} 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() + 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 <> -
- - -
-

Shop in style

-

With this shop hompeage template

-
-
-
- - {state?.shopItemsPagination ? : ''} + + {shopCatalog?.shopItemsPagination ? : ''} } diff --git a/clientapp/src/restClient.ts b/clientapp/src/restClient.ts index 1adfb77..171ddf6 100644 --- a/clientapp/src/restClient.ts +++ b/clientapp/src/restClient.ts @@ -13,7 +13,7 @@ const Post = () => { } -const Get = async (apiUrl: string, props?: IRequest): Promise => { +const Get = async (apiUrl: string, props?: IRequest): Promise => { const url = new URL(apiUrl) if(props) { @@ -40,7 +40,10 @@ const Get = async (apiUrl: string, props?: IRequest): Promise { diff --git a/clientapp/src/store/index.ts b/clientapp/src/store/index.ts index b1a22e6..73001f8 100644 --- a/clientapp/src/store/index.ts +++ b/clientapp/src/store/index.ts @@ -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 diff --git a/clientapp/src/store/reducers/BlogCatalog.ts b/clientapp/src/store/reducers/BlogCatalog.ts new file mode 100644 index 0000000..f276d7a --- /dev/null +++ b/clientapp/src/store/reducers/BlogCatalog.ts @@ -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 => (dispatch, getState) => { + + const apiUrl = 'https://localhost:7151/api/BlogCatalog' + + Get>(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 = (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 +} diff --git a/clientapp/src/store/reducers/Content.ts b/clientapp/src/store/reducers/Content.ts index 52c7d2f..97ac95d 100644 --- a/clientapp/src/store/reducers/Content.ts +++ b/clientapp/src/store/reducers/Content.ts @@ -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 => async (dispatch, getState) => { - + requestContent: (props?: GetStaticContentRequestModel): AppThunkAction => (dispatch, getState) => { + + const apiUrl = 'https://localhost:7151/api/StaticContent' + + Get>(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: `

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

+ ` + }, + featuresSection: { + title: "To help you get started, we have also set up:", + items: [ + { + icon: "navigation", + title: "Client-side navigation", + text: "For example, click Counter then Back to return here." + }, + { + icon: "server", + title: "Development server integration", + text: "In development mode, the development server from create-react-app runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file." + }, + { + icon: "terminal", + title: "Efficient production builds", + text: "In production mode, development-time features are disabled, and your dotnet publish configuration produces minified, efficiently bundled JavaScript files." + } + ] + }, + testimonialsSection: { + items : [ + { + text: "The ClientApp subdirectory is a standard React application based on the create-react-app template. If you open a command prompt in that directory, you can run yarn commands such as yarn test or yarn install.", + 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 = (state: IContentState | undefined, incomingAction: Action): IContentState => { +export const reducer: Reducer = (state: ContentState | undefined, incomingAction: Action): ContentState => { if (state === undefined) { return unloadedState } @@ -57,4 +222,4 @@ export const reducer: Reducer = (state: IContentState | undefined } return state -} \ No newline at end of file +} diff --git a/clientapp/src/store/reducers/Loader.ts b/clientapp/src/store/reducers/Loader.ts new file mode 100644 index 0000000..42cfbe1 --- /dev/null +++ b/clientapp/src/store/reducers/Loader.ts @@ -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 = (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 +} \ No newline at end of file diff --git a/clientapp/src/store/reducers/ShopCatalog.ts b/clientapp/src/store/reducers/ShopCatalog.ts new file mode 100644 index 0000000..36a6881 --- /dev/null +++ b/clientapp/src/store/reducers/ShopCatalog.ts @@ -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 => (dispatch, getState) => { + + const apiUrl = 'https://localhost:7151/api/ShopCatalog' + + Get>(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 = (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 +} diff --git a/webapi/WeatherForecast.sln b/webapi/WeatherForecast.sln index 890930b..035aaf2 100644 --- a/webapi/WeatherForecast.sln +++ b/webapi/WeatherForecast.sln @@ -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 diff --git a/webapi/WeatherForecast/Controllers/BlogCatalogController.cs b/webapi/WeatherForecast/Controllers/BlogCatalogController.cs index 2d6d909..56c0007 100644 --- a/webapi/WeatherForecast/Controllers/BlogCatalogController.cs +++ b/webapi/WeatherForecast/Controllers/BlogCatalogController.cs @@ -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 Categories { get; set; } - - public PaginationModel BlogItemsPagination { get; set; } -} -#endregion - - [AllowAnonymous] [ApiController] [Route("api/[controller]")] @@ -41,37 +30,41 @@ public class BlogCatalogController : ControllerBase { /// [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 { "react", "redux", "webapi" }, - - ReadTime = 10, - Likes = 200, - }; + var blogModels = new List(); - 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 { "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 { CurrentPage = currentPage, - TotalPages = 100, - Items = blogModels + TotalPages = totalPages, + Items = blogModels.Skip((currentPage -1) * itemsPerPage).Take(itemsPerPage).ToList() }, Categories = new List { new CategoryModel { diff --git a/webapi/WeatherForecast/Controllers/ShopCatalogController.cs b/webapi/WeatherForecast/Controllers/ShopCatalogController.cs index 5e236d0..b6c7f79 100644 --- a/webapi/WeatherForecast/Controllers/ShopCatalogController.cs +++ b/webapi/WeatherForecast/Controllers/ShopCatalogController.cs @@ -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 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 { CurrentPage = currentPage, TotalPages = 100, diff --git a/webapi/WeatherForecast/Controllers/StaticContentController.cs b/webapi/WeatherForecast/Controllers/StaticContentController.cs index 9ddaad9..afab634 100644 --- a/webapi/WeatherForecast/Controllers/StaticContentController.cs +++ b/webapi/WeatherForecast/Controllers/StaticContentController.cs @@ -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 { @@ -125,82 +129,84 @@ public class StaticContentController : ControllerBase { } - var pages = new List(); - - pages.Add(new { - Id = "HomePage", - TitleSection = new { - Title = "Hello, World!", + var homePage = new HomePageModel { + TitleSection = new TitleSectionModel { + Title = "Hello, World! by C#", Text = @" -

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

- ", +

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

+ ", Image = new ImageModel { Src = "https://dummyimage.com/600x400/343a40/6c757d", Alt = "..." }, PrimaryLink = new MenuItemModel("Get Started", "#features"), SecondaryLink = new MenuItemModel("Learn More", "#!") }, - FeaturesSection = new { + + 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 Counter then Back to return here." - }, - new { - Icon = "server", - Title = "Development server integration", - Text = "In development mode, the development server from create-react-app runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file." - }, - new { - Icon = "terminal", - Title = "Efficient production builds", - Text = "In production mode, development-time features are disabled, and your dotnet publish configuration produces minified, efficiently bundled JavaScript files." + Items = new List { + new FeatureModel { + Icon = "navigation", + Title = "Client-side navigation", + Text = "For example, click Counter then Back to return here." + }, + new FeatureModel { + Icon = "server", + Title = "Development server integration", + Text = "In development mode, the development server from create-react-app runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file." + }, + new FeatureModel { + Icon = "terminal", + Title = "Efficient production builds", + Text = "In production mode, development-time features are disabled, and your dotnet publish configuration produces minified, efficiently bundled JavaScript files." + } } - } }, - TestimonialsSection = new { - Items = new[] { - new { + TestimonialsSection = new TestimonialsSectionModel { + Items = new List { + new TestimonialModel { Text = "The ClientApp subdirectory is a standard React application based on the create-react-app template. If you open a command prompt in that directory, you can run yarn commands such as yarn test or yarn install.", - Author = new AuthorModel { + 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 }); } } diff --git a/webapi/WeatherForecast/Controllers/WeatherForecastController.cs b/webapi/WeatherForecast/Controllers/WeatherForecastController.cs index cd34fa9..740b41c 100644 --- a/webapi/WeatherForecast/Controllers/WeatherForecastController.cs +++ b/webapi/WeatherForecast/Controllers/WeatherForecastController.cs @@ -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 Get() + public IEnumerable 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)] diff --git a/webapi/WeatherForecast/Dockerfile b/webapi/WeatherForecast/Dockerfile deleted file mode 100644 index 15f0270..0000000 --- a/webapi/WeatherForecast/Dockerfile +++ /dev/null @@ -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"] \ No newline at end of file diff --git a/webapi/WeatherForecast/Models/Abstractions/PageModel.cs b/webapi/WeatherForecast/Models/Abstractions/PageModel.cs new file mode 100644 index 0000000..8046b19 --- /dev/null +++ b/webapi/WeatherForecast/Models/Abstractions/PageModel.cs @@ -0,0 +1,4 @@ + +namespace WeatherForecast.Models.Abstractions { + public abstract class PageModel { } +} diff --git a/webapi/WeatherForecast/Models/Abstractions/PageSectionModel.cs b/webapi/WeatherForecast/Models/Abstractions/PageSectionModel.cs new file mode 100644 index 0000000..e07500f --- /dev/null +++ b/webapi/WeatherForecast/Models/Abstractions/PageSectionModel.cs @@ -0,0 +1,6 @@ +namespace WeatherForecast.Models.Abstractions { + public abstract class PageSectionModel { + public string? Title { get; set; } + public string? Text { get; set; } + } +} diff --git a/webapi/WeatherForecast/Models/Abstractions/PersonModel.cs b/webapi/WeatherForecast/Models/Abstractions/PersonModel.cs new file mode 100644 index 0000000..70c9d15 --- /dev/null +++ b/webapi/WeatherForecast/Models/Abstractions/PersonModel.cs @@ -0,0 +1,6 @@ +namespace WeatherForecast.Models.Abstractions { + public abstract class PersonModel { + public Guid Id { get; set; } + public ImageModel Image { get; set; } + } +} diff --git a/webapi/WeatherForecast/Models/PostItemModel.cs b/webapi/WeatherForecast/Models/Abstractions/PostItemModel.cs similarity index 89% rename from webapi/WeatherForecast/Models/PostItemModel.cs rename to webapi/WeatherForecast/Models/Abstractions/PostItemModel.cs index 0415e0b..39a3414 100644 --- a/webapi/WeatherForecast/Models/PostItemModel.cs +++ b/webapi/WeatherForecast/Models/Abstractions/PostItemModel.cs @@ -1,4 +1,4 @@ -namespace WeatherForecast.Models { +namespace WeatherForecast.Models.Abstractions { public abstract class PostItemModel { public Guid Id { get; set; } diff --git a/webapi/WeatherForecast/Models/AuthorModel.cs b/webapi/WeatherForecast/Models/AuthorModel.cs index a174e2e..d5d049e 100644 --- a/webapi/WeatherForecast/Models/AuthorModel.cs +++ b/webapi/WeatherForecast/Models/AuthorModel.cs @@ -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; } } } diff --git a/webapi/WeatherForecast/Models/BlogItemModel.cs b/webapi/WeatherForecast/Models/BlogItemModel.cs index 19569fa..0c344f5 100644 --- a/webapi/WeatherForecast/Models/BlogItemModel.cs +++ b/webapi/WeatherForecast/Models/BlogItemModel.cs @@ -1,4 +1,6 @@ -namespace WeatherForecast.Models { +using WeatherForecast.Models.Abstractions; + +namespace WeatherForecast.Models { public class BlogItemModel : PostItemModel { diff --git a/webapi/WeatherForecast/Models/FeatureModel.cs b/webapi/WeatherForecast/Models/FeatureModel.cs new file mode 100644 index 0000000..659e62e --- /dev/null +++ b/webapi/WeatherForecast/Models/FeatureModel.cs @@ -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; } + } +} diff --git a/webapi/WeatherForecast/Models/FormItemModel.cs b/webapi/WeatherForecast/Models/FormItemModel.cs new file mode 100644 index 0000000..cdc3d43 --- /dev/null +++ b/webapi/WeatherForecast/Models/FormItemModel.cs @@ -0,0 +1,6 @@ +namespace WeatherForecast.Models { + public class FormItemModel { + public string? Title { get; set; } + public string? PlaceHolder { get; set; } + } +} diff --git a/webapi/WeatherForecast/Models/PageSections/CallToActionSectionModel.cs b/webapi/WeatherForecast/Models/PageSections/CallToActionSectionModel.cs new file mode 100644 index 0000000..f71e68b --- /dev/null +++ b/webapi/WeatherForecast/Models/PageSections/CallToActionSectionModel.cs @@ -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; } + } +} diff --git a/webapi/WeatherForecast/Models/PageSections/FeaturedBologsSectionModel.cs b/webapi/WeatherForecast/Models/PageSections/FeaturedBologsSectionModel.cs new file mode 100644 index 0000000..ec4b02c --- /dev/null +++ b/webapi/WeatherForecast/Models/PageSections/FeaturedBologsSectionModel.cs @@ -0,0 +1,7 @@ +using WeatherForecast.Models.Abstractions; + +namespace WeatherForecast.Models.PageSections { + public class FeaturedBologsSectionModel : PageSectionModel { + public List Items { get; set; } + } +} diff --git a/webapi/WeatherForecast/Models/PageSections/FeaturesSectionModel.cs b/webapi/WeatherForecast/Models/PageSections/FeaturesSectionModel.cs new file mode 100644 index 0000000..bc18649 --- /dev/null +++ b/webapi/WeatherForecast/Models/PageSections/FeaturesSectionModel.cs @@ -0,0 +1,7 @@ +using WeatherForecast.Models.Abstractions; + +namespace WeatherForecast.Models.PageSections { + public class FeaturesSectionModel : PageSectionModel { + public List Items { get; set; } + } +} diff --git a/webapi/WeatherForecast/Models/PageSections/TestimonialsSectionModel.cs b/webapi/WeatherForecast/Models/PageSections/TestimonialsSectionModel.cs new file mode 100644 index 0000000..c7efd7a --- /dev/null +++ b/webapi/WeatherForecast/Models/PageSections/TestimonialsSectionModel.cs @@ -0,0 +1,7 @@ +using WeatherForecast.Models.Abstractions; + +namespace WeatherForecast.Models.PageSections { + public class TestimonialsSectionModel : PageSectionModel { + public List Items { get; set; } + } +} diff --git a/webapi/WeatherForecast/Models/PageSections/TitleSectionModel.cs b/webapi/WeatherForecast/Models/PageSections/TitleSectionModel.cs new file mode 100644 index 0000000..91ef73b --- /dev/null +++ b/webapi/WeatherForecast/Models/PageSections/TitleSectionModel.cs @@ -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; } + } +} diff --git a/webapi/WeatherForecast/Models/Pages/BlogCatalogPageModel.cs b/webapi/WeatherForecast/Models/Pages/BlogCatalogPageModel.cs new file mode 100644 index 0000000..1c7cf0d --- /dev/null +++ b/webapi/WeatherForecast/Models/Pages/BlogCatalogPageModel.cs @@ -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; } + } +} diff --git a/webapi/WeatherForecast/Models/Pages/HomePageModel.cs b/webapi/WeatherForecast/Models/Pages/HomePageModel.cs new file mode 100644 index 0000000..5904305 --- /dev/null +++ b/webapi/WeatherForecast/Models/Pages/HomePageModel.cs @@ -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; } + + } +} diff --git a/webapi/WeatherForecast/Models/Pages/ShopCatalogPageModel.cs b/webapi/WeatherForecast/Models/Pages/ShopCatalogPageModel.cs new file mode 100644 index 0000000..19ddf7d --- /dev/null +++ b/webapi/WeatherForecast/Models/Pages/ShopCatalogPageModel.cs @@ -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; } + } +} diff --git a/webapi/WeatherForecast/Models/Responses/GetBlogCatalogResponseModel.cs b/webapi/WeatherForecast/Models/Responses/GetBlogCatalogResponseModel.cs new file mode 100644 index 0000000..351184f --- /dev/null +++ b/webapi/WeatherForecast/Models/Responses/GetBlogCatalogResponseModel.cs @@ -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 Categories { get; set; } + + public PaginationModel BlogItemsPagination { get; set; } + } +} diff --git a/webapi/WeatherForecast/Models/Responses/GetShopCatalogResponseModel.cs b/webapi/WeatherForecast/Models/Responses/GetShopCatalogResponseModel.cs new file mode 100644 index 0000000..da55dbe --- /dev/null +++ b/webapi/WeatherForecast/Models/Responses/GetShopCatalogResponseModel.cs @@ -0,0 +1,8 @@ +using Core.Abstractions.Models; +using Core.Models; + +namespace WeatherForecast.Models.Responses { + public class GetShopCatalogResponseModel : ResponseModel { + public PaginationModel ShopItemsPagination { get; set; } + } +} diff --git a/webapi/WeatherForecast/Models/Responses/GetStaticContentResponseModel.cs b/webapi/WeatherForecast/Models/Responses/GetStaticContentResponseModel.cs new file mode 100644 index 0000000..11ec255 --- /dev/null +++ b/webapi/WeatherForecast/Models/Responses/GetStaticContentResponseModel.cs @@ -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 Routes { get; set; } + public List AdminRoutes { get; set; } + public List ServiceRoutes { get; set; } + + public List TopMenu { get; set; } + public List SideMenu { get; set; } + + public HomePageModel HomePage { get; set; } + public ShopCatalogPageModel ShopCatalog { get; set; } + public BlogCatalogPageModel BlogCatalog { get; set; } + } +} diff --git a/webapi/WeatherForecast/Models/Responses/GetWeatherForecastResponseModel.cs b/webapi/WeatherForecast/Models/Responses/GetWeatherForecastResponseModel.cs new file mode 100644 index 0000000..237cc89 --- /dev/null +++ b/webapi/WeatherForecast/Models/Responses/GetWeatherForecastResponseModel.cs @@ -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; } + } +} diff --git a/webapi/WeatherForecast/Models/ReviewerModel.cs b/webapi/WeatherForecast/Models/ReviewerModel.cs new file mode 100644 index 0000000..29420ae --- /dev/null +++ b/webapi/WeatherForecast/Models/ReviewerModel.cs @@ -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; } + } +} diff --git a/webapi/WeatherForecast/Models/ShopItemModel.cs b/webapi/WeatherForecast/Models/ShopItemModel.cs index 4eefadd..36ee2dc 100644 --- a/webapi/WeatherForecast/Models/ShopItemModel.cs +++ b/webapi/WeatherForecast/Models/ShopItemModel.cs @@ -1,4 +1,6 @@ -namespace WeatherForecast.Models { +using WeatherForecast.Models.Abstractions; + +namespace WeatherForecast.Models { public class ShopItemModel : PostItemModel { public List Images { get; set; } public string Sku { get; set; } diff --git a/webapi/WeatherForecast/Models/TestimonialModel.cs b/webapi/WeatherForecast/Models/TestimonialModel.cs new file mode 100644 index 0000000..19b4fb4 --- /dev/null +++ b/webapi/WeatherForecast/Models/TestimonialModel.cs @@ -0,0 +1,6 @@ +namespace WeatherForecast.Models { + public class TestimonialModel { + public string Text { get; set; } + public ReviewerModel Reviewer { get; set; } + } +} diff --git a/webapi/WeatherForecast/WeatherForecast.csproj b/webapi/WeatherForecast/WeatherForecast.csproj index 77ee597..41d5ccc 100644 --- a/webapi/WeatherForecast/WeatherForecast.csproj +++ b/webapi/WeatherForecast/WeatherForecast.csproj @@ -6,9 +6,6 @@ enable WeatherForecast true - 2ea970dd-e71a-4c8e-9ff6-2d1d3123d4df - Linux - ..\docker-compose.dcproj @@ -21,4 +18,8 @@ + + + +