`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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ball-clip-rotate-pulse
+
+
+
+
+ ball-clip-rotate-multiple
+
+
+
+
+
+
+
+ ball-zig-zag-deflect
+
+
+
+
+
+
+
+ ball-scale-multiple
+
+
+
+
+
+ line-scale-pulse-out
+
+
+
+ line-scale-pulse-out-rapid
+
+
+
+
+ ball-scale-ripple-multiple
+
+
+
+ ball-spin-fade-loader
+
+
+
+ line-spin-fade-loader
+
+
+
+
+
+
+
+
+
+
\ 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
+
+
+ {firstButton}
+ {prevButton}
+ {prevChunk}
+ {pageButtons}
+ {nextChunk}
+ {nextButton}
+ {lastButton}
+
+
+}
+
+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
+
+
+ {firstButton}
+ {prevButton}
+ {prevChunk}
+ {pageButtons}
+ {nextChunk}
+ {nextButton}
+ {lastButton}
+
+
+}
+
+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
}
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} NavbarBrand>
+ {content?.siteName} NavbarBrand>
- {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
@@ -187,8 +151,8 @@ const CallToActionSection: FC = (props) => {
@@ -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 @@
+
+
+
+