Compare commits

...

10 Commits

161 changed files with 14017 additions and 10795 deletions

22
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,22 @@
image: docker:stable
services:
- docker:dind
before_script:
- docker info
build:
only:
- master
before_script:
- docker login ${CI_REGISTRY} -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD}
script:
# weatherforecast
- docker build ./src/ -f ./src/WeatherForecast/Dockerfile -t ${CI_REGISTRY}/weatherforecast:latest
- docker push ${CI_REGISTRY}/weatherforecast:latest
after_script:
- docker logout ${CI_REGISTRY}
stage: build
tags:
- docker

23
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,23 @@
// Note: Visual studio is using the solution root folder as build context
// docker build -f "<path-to-your-dockerfile>" -t some-name "<!!!path-to-your-solution-folder!!!>"
// Define variable
def CI_REGISTRY = "https://hcrsrv0001.corp.maks-it.com"
def PROJECT_NAME = "reactredux"
def DOCKERHUB_CACHE= "dockerhub_cache/library"
node {
checkout scm
docker.withRegistry("${CI_REGISTRY}", "harbor-credentials-id") {
def clientApp = docker.build("${PROJECT_NAME}/clientapp:${env.BUILD_ID}", "./src/ -f ./src/ClientApp/Dockerfile")
clientApp.push()
def reverseProxy = docker.build("${PROJECT_NAME}/reverseproxy:${env.BUILD_ID}", "./src/ -f ./src/ReverseProxy/Dockerfile")
reverseProxy.push()
def weatherForecast = docker.build("${PROJECT_NAME}/weatherforecast:${env.BUILD_ID}", "./src/ -f ./src/WeatherForecast/Dockerfile")
weatherForecast.push()
}
}

View File

@ -1,496 +1,507 @@
{
"_id": "b3f39a82-6a1b-46a4-85cc-04c3b4315511",
"siteId": "404c8232-9048-4519-bfba-6e78dc7005ca",
"siteName": "MAKS-IT",
"siteUrl": "https://maks-it.com",
"header": {
"title": "{siteName}",
"meta": {
"chartset": "utf-8",
"google-site-verification": "",
"description": "Single-page application home page"
},
"link": {
"canonical": "{siteUrl}"
}
},
"localization": {
"timeZone": "+1",
"locale": "en-US",
"dateFormat": "MMMM YYYY, dddd",
"timeFormat": "HH:mm",
"currency": "EUR",
"currencySymbol": "€"
},
"routes": [
"_id": "404c8232-9048-4519-bfba-6e78dc7005ca",
"l10n": [
{
"target": "/",
"component": "Home"
},
{
"target": "/home",
"component": "Home"
},
{
"target": "/shop",
"childRoutes": [
"locale": 0,
"siteName": "Contoso",
"siteUrl": "https://maks-it.com",
"settings": {
"timeZone": "+1",
"dateFormat": "MMMM YYYY, dddd",
"timeFormat": "HH:mm",
"currency": "EUR",
"currencySymbol": "€"
},
"header": {
"title": "{siteName}",
"meta": {
"chartset": "utf-8",
"google-site-verification": "",
"description": "Single-page application home page"
},
"link": {
"canonical": "{siteUrl}"
}
},
"routes": [
{
"target": "",
"component": "ShopCatalog"
"target": "/",
"component": "Home"
},
{
"target": ":page",
"component": "ShopCatalog"
"target": "/home",
"component": "Home"
},
{
"target": ":page",
"childRoutes": [
{
"target": ":slug",
"component": "ShopItem"
}
]
},
{
"target": "cart",
"target": "/shop",
"childRoutes": [
{
"target": "",
"component": "Cart"
"component": "ShopCatalog"
},
{
"target": "checkout",
"component": "Checkout"
"target": ":page",
"component": "ShopCatalog"
},
{
"target": ":page",
"childRoutes": [
{
"target": ":slug",
"component": "ShopItem"
}
]
},
{
"target": "cart",
"childRoutes": [
{
"target": "",
"component": "Cart"
},
{
"target": "checkout",
"component": "Checkout"
}
]
}
]
}
]
},
{
"target": "/blog",
"childRoutes": [
{
"target": "",
"component": "BlogCatalog"
},
{
"target": ":page",
"component": "BlogCatalog"
},
{
"target": ":page",
"target": "/blog",
"childRoutes": [
{
"target": ":slug",
"component": "BlogItem"
"target": "",
"component": "BlogCatalog"
},
{
"target": ":page",
"component": "BlogCatalog"
},
{
"target": ":page",
"childRoutes": [
{
"target": ":slug",
"component": "BlogItem"
}
]
}
]
}
]
}
],
"adminRoutes": [],
"serviceRoutes": [
{
"target": "/signin",
"component": "Signin"
},
{
"target": "/signup",
"component": "Signup"
}
],
"topMenu": [
{
"target": "/",
"title": "Home"
},
{
"target": "/shop",
"title": "Shop"
},
{
"target": "/blog",
"title": "Blog"
},
{
"target": "/signin",
"title": "Sing in"
},
{
"target": "/signup",
"title": "Sign up"
},
{
"target": "/shop/cart",
"icon": "shopping-cart",
"title": "Cart ({quantity})"
}
],
"sideMenu": [],
"homePage": {
"header": {
"title": "Home - {siteName}",
"meta": {
"description": "Single-page application home page"
},
"link": {
"canonical": "{siteUrl}"
}
},
"titleSection": {
"title": "Hello, World! by C# and Mongo",
"text": "<p>Welcome to your new single-page application, built with:</p>\n <ul>\n <li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'>C#</a> for cross-platform server-side code</li>\n <li><a href='https://facebook.github.io/react/'>React</a> and <a href='https://redux.js.org/'>Redux</a> for client-side code</li>\n <li><a href='https://getbootstrap.com/'>Bootstrap</a>, <a href='https://reactstrap.github.io/?path=/story/home-installation--page'>Reactstrap</a> and <a href=\"\"https://feathericons.com/\"\">Feather icons</a> for layout and styling</li>\n </ul>",
"primaryLink": {
"target": "#!",
"anchorText": "Get Started"
},
"secondaryLink": {
"target": "#!",
"anchorText": "Learn more"
},
"image": {
"src": "https://dummyimage.com/600x400/343a40/6c757d",
"alt": "..."
}
},
"featuresSection": {
"title": "To help you get started, we have also set up:",
"items": [
],
"adminRoutes": [],
"serviceRoutes": [
{
"icon": "navigation",
"title": "Client-side navigation",
"text": "For example, click <em>Counter</em> then <em>Back</em> to return here."
"target": "/signin",
"component": "Signin"
},
{
"icon": "server",
"title": "Development server integration",
"text": "In development mode, the development server from <code>create-react-app</code> runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file."
},
{
"icon": "terminal",
"title": "Efficient production builds",
"text": "In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration produces minified, efficiently bundled JavaScript files."
"target": "/signup",
"component": "Signup"
}
]
},
"testimonialsSection": {
"items": [
],
"topMenu": [
{
"text": "The <code>ClientApp</code> subdirectory is a standard React application based on the <code>create-react-app</code> template. If you open a command prompt in that directory, you can run <code>yarn</code> commands such as <code>yarn test</code> or <code>yarn install</code>.",
"reviewer": {
"_id": "c5295208-8950-441f-8217-bd7c4a907a0f",
"image": {
"src": "https://dummyimage.com/40x40/ced4da/6c757d",
"alt": "..."
"target": "/",
"title": "Home"
},
{
"target": "/shop",
"title": "Shop"
},
{
"target": "/blog",
"title": "Blog"
},
{
"target": "/signin",
"title": "Sign in"
},
{
"target": "/signup",
"title": "Sign up"
},
{
"target": "/shop/cart",
"icon": "shopping-cart",
"title": "Cart ({quantity})"
}
],
"sideMenu": [],
"homePage": {
"header": {
"title": "Home - {siteName}",
"meta": {
"description": "Single-page application home page"
},
"link": {
"canonical": "{siteUrl}"
}
},
"titleSection": {
"title": "Hello, World! by C# and Mongo",
"text": "<p>Welcome to your new single-page application, built with:</p>\n <ul>\n <li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'>C#</a> for cross-platform server-side code</li>\n <li><a href='https://facebook.github.io/react/'>React</a> and <a href='https://redux.js.org/'>Redux</a> for client-side code</li>\n <li><a href='https://getbootstrap.com/'>Bootstrap</a>, <a href='https://reactstrap.github.io/?path=/story/home-installation--page'>Reactstrap</a> and <a href=\"\"https://feathericons.com/\"\">Feather icons</a> for layout and styling</li>\n </ul>",
"primaryLink": {
"target": "#!",
"anchorText": "Get Started"
},
"secondaryLink": {
"target": "#!",
"anchorText": "Learn more"
},
"image": {
"src": "/Image/600x400/343a40/6c757d",
"alt": "..."
}
},
"featuresSection": {
"title": "To help you get started, we have also set up:",
"items": [
{
"icon": "navigation",
"title": "Client-side navigation",
"text": "For example, click <em>Counter</em> then <em>Back</em> to return here."
},
"fullName": "Admin",
"position": "CEO, MAKS-IT"
{
"icon": "server",
"title": "Development server integration",
"text": "In development mode, the development server from <code>create-react-app</code> runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file."
},
{
"icon": "terminal",
"title": "Efficient production builds",
"text": "In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration produces minified, efficiently bundled JavaScript files."
}
]
},
"testimonialsSection": {
"items": [
{
"text": "The <code>ClientApp</code> subdirectory is a standard React application based on the <code>create-react-app</code> template. If you open a command prompt in that directory, you can run <code>yarn</code> commands such as <code>yarn test</code> or <code>yarn install</code>.",
"reviewer": {
"image": {
"src": "/Image/40x40/ced4da/6c757d",
"alt": "..."
},
"fullName": "Admin",
"position": "CEO, MAKS-IT"
}
}
]
},
"featuredBlogsSection": {
"title": "Featured blogs"
},
"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..."
}
}
]
},
"featuredBlogsSection": {
"title": "Featured blogs"
},
"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": {
},
"shopCatalog": {
"header": {
"title": "Shop catalog - {siteName}",
"meta": {
"description": "Single-page application shop catalog"
},
"link": {
"canonical": ""
}
},
"titleSection": {
"title": "Shop in style",
"text": "With this shop hompeage template"
},
"shopItemsSection": {
"addToCart": "Add to cart"
}
},
"shopItem": {
"header": {
"title": "{productTitle} - {siteName}",
"meta": {
"description": "Single-page application shop item"
},
"link": {
"canonical": ""
}
},
"productSection": {
"availableQuantity": "Available Qty.",
"addToCart": "Add to cart"
},
"relatedProductsSection": {
"title": "Related products",
"addToCart": "Add to cart"
}
},
"shopCart": {
"header": {
"title": "Shop cart - {siteName}",
"meta": {
"description": "Single-page application shop cart"
},
"link": {
"canonical": ""
}
},
"titleSection": {
"title": "Shopping Cart",
"text": "<i class=\"text-info font-weight-bold\">{quantity}</i> items in your cart"
},
"productsSection": {
"product": "Product",
"price": "Price",
"quantity": "Quantity",
"subtotal": "Subtotal:",
"continueShopping": {
"target": "/shop",
"anchorText": "Continue shopping"
},
"checkout": {
"target": "checkout",
"anchorText": "Checkout"
}
}
},
"shopCheckout": {
"header": {
"title": "Shop - checkout {siteName}",
"meta": {
"description": "Single-page application checkout"
},
"link": {
"canonical": ""
}
},
"titleSection": {
"title": "Checkout",
"text": "Below is an example form built entirely with Bootstrap's form controls. Each required form group has a validation state that can be triggered by attempting to submit the form without completing it."
},
"billingAddressSection": {
"title": "Billing address",
"firstName": {
"title": "First name",
"placeHolder": "First name..."
},
"lastName": {
"title": "Last name",
"placeHolder": "Last name..."
},
"address": {
"title": "Address",
"placeHolder": "1234 Main Str.."
},
"address2": {
"title": "Address",
"optional": "(Optional)",
"placeHolder": "1234 Main Str.."
},
"country": {
"title": "Country",
"placeHolder": "Country..."
},
"state": {
"title": "State",
"placeHolder": "State..."
},
"city": {
"title": "City",
"placeHolder": "City..."
},
"zip": {
"title": "Zip",
"placeHolder": "Zip..."
}
},
"shippingAddressSection": {
"title": "Shipping address",
"firstName": {
"title": "First name",
"placeHolder": "First name..."
},
"lastName": {
"title": "Last name",
"placeHolder": "Last name..."
},
"address": {
"title": "Address",
"placeHolder": "1234 Main Str.."
},
"address2": {
"title": "Address",
"optional": "(Optional)",
"placeHolder": "1234 Main Str.."
},
"country": {
"title": "Country",
"placeHolder": "Country..."
},
"state": {
"title": "State",
"placeHolder": "State..."
},
"city": {
"title": "City",
"placeHolder": "City..."
},
"zip": {
"title": "Zip",
"placeHolder": "Zip..."
}
},
"settingsSection": {
"shippingAddressSameAsBillingAddress": "Shipping address is the same as my billing address",
"saveThisInformation": "Save this information for next time"
},
"summarySection": {
"title": "Your cart",
"total": "Total ({currency})",
"promoCode": {
"placeHolder": "Promo code"
},
"submit": {
"title": "Redeem"
}
},
"paymentSection": {
"title": "Payment",
"nameOnCard": {
"title": "Name on card",
"placeHolder": "John Doe"
},
"cardNumber": {
"title": "Credit card number",
"placeHolder": ""
},
"expiration": {
"title": "Expiration",
"placeHolder": "MM/YY"
},
"cvv": {
"title": "CVV",
"placeHolder": "123"
}
},
"submit": {
"title": "Continue to checkout"
}
},
"blogCatalog": {
"header": {
"title": "Blog catalog - {siteName}",
"meta": {
"description": "Single-page application blog catalog"
},
"link": {
"canonical": ""
}
},
"titleSection": {
"title": "Welcome to Blog Home!",
"text": "A Bootstrap 5 starter layout for your next blog homepage"
},
"featuredBlogSection": {
"readTime": "{date} Time to read: {readTime} min"
}
},
"blogItem": {
"header": {
"title": "{blogTitle} - {siteName}",
"meta": {
"description": "Single-page application blog item"
},
"link": {
"canonical": ""
}
},
"titleSection": {
"postedOnBy": "Posted on {date} by {nickName}"
},
"commentsSection": {
"leaveComment": "Join the discussion and leave a comment!"
}
},
"signIn": {
"header": {
"title": "Sign in - {siteName}",
"meta": {
"description": "Single-page application sign in",
"robots": "noindex, nofollow"
},
"link": {
"canonical": ""
}
},
"title": "Sign in",
"email": {
"title": "Email address",
"placeHolder": "Email address..."
},
"password": {
"title": "Password",
"placeHolder": "Password..."
},
"dontHaveAnAccount": "Don't have an account yet? Please",
"signUpLink": {
"target": "/signup",
"anchorText": "Sign up"
},
"submit": {
"title": "Sign in"
}
},
"signUp": {
"header": {
"title": "Sign up - {siteName}",
"meta": {
"description": "Single-page application sign up",
"robots": "noindex, nofollow"
},
"link": {
"canonical": ""
}
},
"title": "Sign up",
"placeHolder": "Email address..."
"username": {
"title": "Username",
"placeHolder": "Username..."
},
"email": {
"title": "Email address",
"placeHolder": "Email address..."
},
"reEmail": {
"title": "Repeat email address",
"placeHolder": "Repeat email address..."
},
"password": {
"title": "Password",
"placeHolder": "Password..."
},
"rePassword": {
"title": "Repeat password",
"placeHolder": "Repeat password..."
},
"acceptTermsAndConditions": "Accept terms and conditions",
"submit": {
"title": "Sing up"
}
}
}
},
"shopCatalog": {
"header": {
"title": "Shop catalog - {siteName}",
"meta": {
"description": "Single-page application shop catalog"
},
"link": {
"canonical": ""
}
},
"titleSection": {
"title": "Shop in style",
"text": "With this shop hompeage template"
},
"shopItemsSection": {
"addToCart": "Add to cart"
}
},
"shopItem": {
"header": {
"title": "{productTitle} - {siteName}",
"meta": {
"description": "Single-page application shop item"
},
"link": {
"canonical": ""
}
},
"productSection": {
"availableQuantity": "Available Qty.",
"addToCart": "Add to cart"
},
"relatedProductsSection": {
"title": "Related products",
"addToCart": "Add to cart"
}
},
"shopCart": {
"header": {
"title": "Shop cart - {siteName}",
"meta": {
"description": "Single-page application shop cart"
},
"link": {
"canonical": ""
}
},
"titleSection": {
"title": "Shopping Cart",
"text": "<i class=\"text-info font-weight-bold\">{quantity}</i> items in your cart"
},
"productsSection": {
"product": "Product",
"price": "Price",
"quantity": "Quantity",
"subtotal": "Subtotal:",
"continueShopping": {
"target": "/shop",
"anchorText": "Continue shopping"
},
"checkout": {
"target": "checkout",
"anchorText": "Checkout"
}
}
},
"shopCheckout": {
"header": {
"title": "Shop - checkout {siteName}",
"meta": {
"description": "Single-page application checkout"
},
"link": {
"canonical": ""
}
},
"titleSection": {
"title": "Checkout",
"text": "Below is an example form built entirely with Bootstraps form controls. Each required form group has a validation state that can be triggered by attempting to submit the form without completing it."
},
"billingAddressSection": {
"title": "Billing address",
"firstName": {
"title": "First name",
"placeHolder": "First name..."
},
"lastName": {
"title": "Last name",
"placeHolder": "Last name..."
},
"address": {
"title": "Address",
"placeHolder": "1234 Main Str.."
},
"address2": {
"title": "Address",
"optional": "(Optional)",
"placeHolder": "1234 Main Str.."
},
"country": {
"title": "Country",
"placeHolder": "Country..."
},
"state": {
"title": "State",
"placeHolder": "State..."
},
"city": {
"title": "City",
"placeHolder": "City..."
},
"zip": {
"title": "Zip",
"placeHolder": "Zip..."
}
},
"shippingAddressSection": {
"title": "Shipping address",
"firstName": {
"title": "First name",
"placeHolder": "First name..."
},
"lastName": {
"title": "Last name",
"placeHolder": "Last name..."
},
"address": {
"title": "Address",
"placeHolder": "1234 Main Str.."
},
"address2": {
"title": "Address",
"optional": "(Optional)",
"placeHolder": "1234 Main Str.."
},
"country": {
"title": "Country",
"placeHolder": "Country..."
},
"state": {
"title": "State",
"placeHolder": "State..."
},
"city": {
"title": "City",
"placeHolder": "City..."
},
"zip": {
"title": "Zip",
"placeHolder": "Zip..."
}
},
"settingsSection": {
"shippingAddressSameAsBillingAddress": "Shipping address is the same as my billing address",
"saveThisInformation": "Save this information for next time"
},
"summarySection": {
"title": "Your cart",
"total": "Total ({currency})",
"promoCode": {
"placeHolder": "Promo code"
},
"submit": {
"title": "Redeem"
}
},
"paymentSection": {
"title": "Payment",
"nameOnCard": {
"title": "Name on card",
"placeHolder": "John Doe"
},
"cardNumber": {
"title": "Credit card number",
"placeHolder": ""
},
"expiration": {
"title": "Expiration",
"placeHolder": "MM/YY"
},
"cvv": {
"title": "CVV",
"placeHolder": "123"
}
},
"submit": {
"title": "Continue to checkout"
}
},
"blogCatalog": {
"header": {
"title": "Blog catalog - {siteName}",
"meta": {
"description": "Single-page application blog catalog"
},
"link": {
"canonical": ""
}
},
"titleSection": {
"title": "Welcome to Blog Home!",
"text": "A Bootstrap 5 starter layout for your next blog homepage"
},
"featuredBlogSection": {
"readTime": "{date} Time to read: {readTime} min"
}
},
"blogItem": {
"header": {
"title": "{blogTitle} - {siteName}",
"meta": {
"description": "Single-page application blog item"
},
"link": {
"canonical": ""
}
},
"titleSection": {
"postedOnBy": "Posted on {date} by {nickName}"
},
"commentsSection": {
"leaveComment": "Join the discussion and leave a comment!"
}
},
"signIn": {
"header": {
"title": "Sign in - {siteName}",
"meta": {
"description": "Single-page application sign in",
"robots": "noindex, nofollow"
},
"link": {
"canonical": ""
}
},
"title": "Sign in",
"email": {
"title": "Email address",
"placeHolder": "Email address..."
},
"password": {
"title": "Password",
"placeHolder": "Password..."
},
"dontHaveAnAccount": "Don't have an account yet? Please",
"signUpLink": {
"target": "/signup",
"anchorText": "Sign up"
},
"submit": {
"title": "Sign in"
}
},
"signUp": {
"header": {
"title": "Sign up - {siteName}",
"meta": {
"description": "Single-page application sign up",
"robots": "noindex, nofollow"
},
"link": {
"canonical": ""
}
},
"title": "Sign up",
"username": {
"title": "Username",
"placeHolder": "Username..."
},
"email": {
"title": "Email address",
"placeHolder": "Email address..."
},
"reEmail": {
"title": "Repeat email address",
"placeHolder": "Repeat email address..."
},
"password": {
"title": "Password",
"placeHolder": "Password..."
},
"rePassword": {
"title": "Repeat password",
"placeHolder": "Repeat password..."
},
"acceptTermsAndConditions": "Accept terms and conditions",
"submit": {
"title": "Sing up"
}
}
]
}

View File

@ -1,3 +1,6 @@
*
!obj\Docker\publish\*
!obj\Docker\empty\
**/.dockerignore
**/.vs
**/.vscode
**/Dockerfile*
**/node_modules
!**/.env

View File

@ -1,7 +1,12 @@
BROWSER=none
REACT_APP_API=https://localhost:7151/api
REACT_APP_LOCAL_ONLY=N
REACT_APP_FRONTEND=https://localhost:7174
REACT_APP_API=https://localhost:7174/api
REACT_APP_SITEID=404c8232-9048-4519-bfba-6e78dc7005ca
REACT_APP_LOCALE=en-US
REACT_APP_ACCOUNT=Account

23
src/ClientApp/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -1,22 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
},
{
"type": "pwa-msedge",
"request": "launch",
"name": "Launch Edge against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@ -37,6 +37,7 @@
<DependentUpon>Dockerfile</DependentUpon>
</Content>
<None Include="Dockerfile" />
<Content Include=".env" />
<Content Include="src\components\FeatherIcons\icons.json" />
<Content Include="src\components\Loader\scss\animations\ball-beat.scss" />
<Content Include="src\components\Loader\scss\animations\ball-clip-rotate-multiple.scss" />
@ -98,7 +99,6 @@
<Content Include="src\pages\Shop\Checkout\scss\style.module.scss" />
<Content Include="src\pages\Signin\scss\style.scss" />
<Content Include="src\pages\Signup\scss\style.scss" />
<Content Include="tsconfig.json" />
<Content Include="package.json" />
<Content Include="README.md" />
</ItemGroup>
@ -121,6 +121,7 @@
<Folder Include="src\enumerations\" />
<Folder Include="src\functions\" />
<Folder Include="src\functions\jwtDecode\" />
<Folder Include="src\interfaces\" />
<Folder Include="src\layouts\" />
<Folder Include="src\layouts\admin\" />
<Folder Include="src\layouts\admin\NavMenu\" />
@ -132,7 +133,6 @@
<Folder Include="src\layouts\public\NavMenu\" />
<Folder Include="src\layouts\public\scss\" />
<Folder Include="src\layouts\scss\" />
<Folder Include="src\models\" />
<Folder Include="src\pages\" />
<Folder Include="src\pages\Blog\" />
<Folder Include="src\pages\Blog\Catalog\" />
@ -183,6 +183,7 @@
<TypeScriptCompile Include="src\functions\jwtDecode\base64_url_decode.ts" />
<TypeScriptCompile Include="src\functions\jwtDecode\index.ts" />
<TypeScriptCompile Include="src\index.tsx" />
<TypeScriptCompile Include="src\interfaces\index.ts" />
<TypeScriptCompile Include="src\layouts\admin\index.tsx" />
<TypeScriptCompile Include="src\layouts\admin\NavMenu\index.tsx" />
<TypeScriptCompile Include="src\layouts\admin\SideMenu\index.tsx" />
@ -191,12 +192,6 @@
<TypeScriptCompile Include="src\layouts\public\Footer\index.tsx" />
<TypeScriptCompile Include="src\layouts\public\index.tsx" />
<TypeScriptCompile Include="src\layouts\public\NavMenu\index.tsx" />
<TypeScriptCompile Include="src\models\abstractions.ts" />
<TypeScriptCompile Include="src\models\index.ts" />
<TypeScriptCompile Include="src\models\pages.ts" />
<TypeScriptCompile Include="src\models\pageSections.ts" />
<TypeScriptCompile Include="src\models\requests.ts" />
<TypeScriptCompile Include="src\models\responses.ts" />
<TypeScriptCompile Include="src\pages\AdminHome.tsx" />
<TypeScriptCompile Include="src\pages\Blog\Catalog\index.tsx" />
<TypeScriptCompile Include="src\pages\Blog\index.ts" />
@ -226,7 +221,6 @@
<TypeScriptCompile Include="src\store\reducers\Content.ts" />
<TypeScriptCompile Include="src\store\reducers\Counter.ts" />
<TypeScriptCompile Include="src\store\reducers\Header.ts" />
<TypeScriptCompile Include="src\store\reducers\Loader.ts" />
<TypeScriptCompile Include="src\store\reducers\ShopCart.ts" />
<TypeScriptCompile Include="src\store\reducers\ShopCatalog.ts" />
<TypeScriptCompile Include="src\store\reducers\ShopCategories.ts" />

View File

@ -1,19 +1,15 @@
# ==== CONFIGURE =====
# Use a Node 16 base image
FROM node:16-alpine
# Set the working directory to /app inside the container
FROM node:lts as base
WORKDIR /app
# Copy app files
COPY . .
# ==== BUILD =====
# Install dependencies (npm ci makes sure the exact versions in the lockfile gets installed)
RUN npm ci
# Build the app
RUN npm run build
# ==== RUN =======
# Set the env to "production"
ENV NODE_ENV production
# Expose the port on which the app will be running (3000 is the default that `serve` uses)
ENV CI=true
ENV PORT=3000
EXPOSE 3000
# Start the app
CMD [ "npx", "serve", "build" ]
FROM base as build
COPY ["ClientApp/", "/app"]
RUN npm i --package-lock-only
RUN npm ci
CMD [ "npm", "start" ]
#FROM build as final
#COPY ["ClientApp/build", "/app"]

View File

@ -1,3 +1,46 @@
# ClientApp
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +1,59 @@
{
"name": "react-redux-template",
"name": "reactredux",
"version": "0.1.0",
"private": true,
"dependencies": {
"bootstrap": "5.1.3",
"classnames": "^2.3.1",
"dayjs": "^1.11.2",
"history": "5.3.0",
"@babel/plugin-proposal-private-property-in-object": "^7.21.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.33",
"@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4",
"axios": "^1.4.0",
"bootstrap": "^5.2.3",
"classnames": "^2.3.2",
"dayjs": "^1.11.7",
"history": "^5.3.0",
"merge": "^2.1.1",
"react": "18.0.0",
"react-dom": "18.0.0",
"react-redux": "7.2.8",
"react-router": "6.3.0",
"react-router-dom": "6.3.0",
"reactstrap": "9.0.2",
"redux": "4.1.2",
"redux-first-history": "^5.0.9",
"redux-thunk": "2.4.1",
"svgo": "2.8.0"
},
"devDependencies": {
"@types/history": "4.7.11",
"@types/jest": "27.4.1",
"@types/node": "17.0.24",
"@types/react": "18.0.5",
"@types/react-dom": "18.0.1",
"@types/react-redux": "7.1.24",
"@types/react-router": "5.1.18",
"@types/react-router-dom": "5.3.3",
"@typescript-eslint/parser": "^5.19.0",
"cross-env": "7.0.3",
"eslint": "^8.13.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"nan": "^2.15.0",
"react-scripts": "^5.0.1",
"sass": "^1.50.0",
"typescript": "4.6.3"
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-router": "^6.11.2",
"react-router-dom": "^6.11.2",
"react-scripts": "5.0.1",
"reactstrap": "^9.1.10",
"redux": "^4.2.1",
"redux-first-history": "^5.1.1",
"redux-thunk": "^2.4.2",
"sass": "^1.62.1",
"svgo": "^3.0.2",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "cross-env CI=true react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint ./src/**/*.ts ./src/**/*.tsx"
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
"extends": [
"react-app",
"react-app/jest"
]
},
"resolutions": {
"url-parse": ">=1.5.0",
"lodash": ">=4.17.21"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,16 +1,20 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<base href="%PUBLIC_URL%/" />
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
@ -20,12 +24,10 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>react-redux-template</title>
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,14 +1,24 @@
{
"short_name": "react-redux-template",
"name": "react-redux-template",
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "./index.html",
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,28 +1,38 @@
// React
import React, { FC, useEffect } from 'react'
import React, { FC, useEffect, useState } from 'react'
import { Route, Routes, useLocation } from 'react-router'
// Redux
import { useSelector, useDispatch } from 'react-redux'
import { actionCreators as settingsActionCreators } from './store/reducers/Content'
import type {} from 'redux-thunk/extend-redux'
import { actionCreators as contentActionCreators } from './store/reducers/Content'
// Components
import { DynamicLayout } from './layouts'
import { DynamicPage } from './pages'
import { RouteModel } from './models'
import { ApplicationState } from './store'
import { Loader } from './components/Loader'
import { Helmet } from './components/ReactHelmet'
import axios from 'axios'
import ModalComponent from './components/Modal'
export interface IRoute {
target: string
component?: string
childRoutes?: IRoute []
}
interface IRouteProp {
path: string,
element?: JSX.Element
}
const NestedRoutes = (routes: RouteModel[], tag: string | undefined = undefined) => {
const NestedRoutes = (routes: IRoute[], tag: string | undefined = undefined) => {
if(!Array.isArray(routes)) return
return routes.map((route: RouteModel, index: number) => {
return routes.map((route: IRoute, index: number) => {
const routeProps: IRouteProp = {
path: route.target
}
@ -32,20 +42,51 @@ const NestedRoutes = (routes: RouteModel[], tag: string | undefined = undefined)
routeProps.element = tag ? <DynamicLayout tag={tag}>{page}</DynamicLayout> : page
}
return <Route key={index} { ...routeProps }>{Array.isArray(route.childRoutes) ? NestedRoutes(route.childRoutes, tag) : ''}</Route>
})
}
const App = () => {
const [loaderCounter, setLoaderCounter] = useState<number>(0)
// Add a request interceptor
axios.interceptors.request.use((config) => {
setLoaderCounter(loaderCounter + 1)
// Do something before request is sent
return config
}, err => {
// console.log(err)
setLoaderCounter(0)
// Do something with request error
return Promise.reject(err)
})
// Add a response interceptor
axios.interceptors.response.use(function (response) {
setLoaderCounter(loaderCounter > 0 ? loaderCounter - 1 : 0)
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response
}, err => {
// console.log(err)
setLoaderCounter(0)
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(err)
})
const { pathname } = useLocation()
const dispatch = useDispatch()
const { content, header, loader } = useSelector((state: ApplicationState) => state)
const { content, header } = useSelector((state: ApplicationState) => state)
useEffect(() => {
dispatch(settingsActionCreators.requestContent())
dispatch(contentActionCreators.requestContent())
}, [])
useEffect(() => {
@ -56,11 +97,7 @@ const App = () => {
}, [pathname])
const {
title = "",
link = {},
meta = {}
} = header ? header : {}
const { title, link, meta } = header
return <>
<Helmet>
@ -76,7 +113,13 @@ const App = () => {
{content?.serviceRoutes ? NestedRoutes(content.serviceRoutes) : ''}
</Routes>
{loader ? <Loader {...loader} /> : ''}
{<ModalComponent {...{
visible: false,
title: 'Message title',
text: 'Some message'
}} />}
{<Loader {...{visible: loaderCounter > 0 }} />}
</>
}

View File

@ -1,4 +1,4 @@
import React, { FC, ReactNode, useEffect, useState } from 'react'
import React, { FC, ReactNode } from 'react'
import './scss/loaders.scss'

View File

@ -0,0 +1,40 @@
import React, { FC, useState } from 'react'
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'
export interface IModalComponent {
visible: boolean
title: string,
text: string
}
const ModalComponent : FC<IModalComponent> = (props) => {
const { visible, title, text } = props
const [modal, setModal] = useState(visible)
const toggle = () => setModal(!modal)
return (
<div>
{/* <Button color="danger" onClick={toggle}>
Click Me
</Button> */}
<Modal isOpen={modal} toggle={toggle}>
<ModalHeader toggle={toggle}>{title}</ModalHeader>
<ModalBody>{text}</ModalBody>
{/*<ModalFooter>
<Button color="primary" onClick={toggle}>
Do Something
</Button>{' '}
<Button color="secondary" onClick={toggle}>
Cancel
</Button>
</ModalFooter>*/}
</Modal>
</div>
)
}
export default ModalComponent

View File

@ -4,14 +4,14 @@ import { Pagination as ReactstrapPagination, PaginationItem, PaginationLink } fr
import { findChunk, intToArray, splitInChunks } from './utils'
interface PaginationProps {
export interface IPaginationComponent {
maxVisiblePages?: number,
totalPages: number,
currentPage: number,
onClick: (page: number) => void
}
const Pagination: FC<PaginationProps> = ({
const Pagination: FC<IPaginationComponent> = ({
maxVisiblePages = 5,
totalPages = 1,
currentPage = 1,
@ -80,14 +80,14 @@ const Pagination: FC<PaginationProps> = ({
</nav>
}
interface SSRPaginationProps {
export interface ISSRPaginationComponent {
maxVisiblePages?: number,
totalPages: number,
currentPage: number,
linksPath?: string
}
const SSRPagination: FC<SSRPaginationProps> = ({
const SSRPagination: FC<ISSRPaginationComponent> = ({
maxVisiblePages = 5,
totalPages = 1,
currentPage = 1,

View File

@ -1,14 +1,25 @@
import React, { FC } from 'react'
import { Card, CardBody, CardHeader, Col, Row } from 'reactstrap'
import { CategoryModel } from '../../models'
import { Link } from 'react-router-dom'
interface ICategories {
items?: CategoryModel []
import { Card, CardBody, CardHeader, Col, Row } from 'reactstrap'
export interface ICategory {
href: string,
anchorText: string
}
const Categories: FC<ICategories> = ({
items = []
}) => {
interface ICategories {
totalPages: number,
currentPage: number,
items?: ICategory []
}
export interface ICategoriesComponent extends ICategories {}
const Categories: FC<ICategoriesComponent> = (props) => {
const { items = [] } = props
const middleIndex = Math.ceil(items.length / 2)
const firstHalf = items.splice(0, middleIndex)
@ -21,12 +32,12 @@ const Categories: FC<ICategories> = ({
<Col sm="6">
<ul className="list-unstyled mb-0">
{firstHalf.map((item, index) => <li key={index}><a href="#!">{item.text}</a></li>)}
{firstHalf.map((item, index) => <li key={index}><Link to={item.href}>{item.anchorText}</Link></li>)}
</ul>
</Col>
<Col sm="6">
<ul className="list-unstyled mb-0">
{secondHalf.map((item, index) => <li key={index}><a href="#!">{item.text}</a></li>)}
{secondHalf.map((item, index) => <li key={index}><Link to={item.href}>{item.anchorText}</Link></li>)}
</ul>
</Col>
</Row>
@ -36,4 +47,4 @@ const Categories: FC<ICategories> = ({
export {
Categories
}
}

View File

@ -0,0 +1,10 @@
const cloneObject = <T>(obj : T) => {
const json = JSON.stringify(obj)
const newObj: T = JSON.parse(json)
return newObj
}
export {
cloneObject
}

View File

@ -1,16 +1,24 @@
import { RouteModel } from "../models"
import { IRoute } from "../App"
interface ComponentRoutesModel {
targets: string [],
component: string
}
const findRoutes = (routes: RouteModel[] = [], component: string | undefined, parentTarget: string [] = [], result: ComponentRoutesModel [] = []): ComponentRoutesModel [] => {
/**
* Usege const path = findRoutes(content?.routes, 'ShopCatalog')[0]?.targets[0]
* @param routes
* @param component
* @param parentTarget
* @param result
* @returns
*/
const findRoutes = (routes: IRoute[] = [], component: string | undefined, parentTarget: string [] = [], result: ComponentRoutesModel [] = []): ComponentRoutesModel [] => {
if(!Array.isArray(routes))
return []
routes.forEach((route: RouteModel) => {
routes.forEach((route: IRoute) => {
const targets: string [] = []
if(parentTarget) {
parentTarget.forEach(item => {

View File

@ -1,10 +1,14 @@
import { dateFormat, timeFormat } from './dateTimeFormat'
import { findRoutes } from './findRoutes'
import { getKeyValue } from './getKeyValue'
import { cloneObject } from './cloneObject'
import { isSuccessStatusCode } from './isSuccessStatusCode'
export {
getKeyValue,
dateFormat,
timeFormat,
findRoutes
findRoutes,
cloneObject,
isSuccessStatusCode
}

View File

@ -0,0 +1,7 @@
const isSuccessStatusCode = (statusCode: number) => {
return statusCode >= 200 && statusCode <= 299
}
export {
isSuccessStatusCode
}

View File

@ -0,0 +1,123 @@
//new shared interfaces
interface IHeaderLink {
[key: string]: string
}
interface IMeta {
[key: string]: string
}
export interface IHeader {
title: string,
link: IHeaderLink,
meta: IMeta
}
export interface IParams {
[key: string]: string | undefined
}
export interface IRequest<T, TT> {
pathParams?: T
searchParams?: TT,
}
export interface IResponse {}
export interface IPagination<T> {
totalPages: number,
currentPage: number,
items: T []
}
// export interface PaginationModel<T> {
// totalPages: number,
// currentPage: number,
// items: T []
// }
// export interface AuthorModel extends PersonModel {
// nickName: string
// }
// export interface BlogItemModel extends PostItemModel {
// readTime?: number,
// likes?: number
// }
// export interface FeatureModel {
// icon: string,
// title: string,
// text: string
// }
// export interface FormItemModel {
// title?: string,
// optional?: string,
// placeHolder?: string,
// }
// export interface LinkModel {
// target: string,
// anchorText: string
// }
// export interface ReviewerModel extends PersonModel {
// fullName: string,
// position: string
// }
// export interface ShopItemModel extends PostItemModel {
// images?: ImageModel [],
// sku: string,
// brandName: string,
// rating?: number,
// price: number,
// newPrice?: number,
// quantity?: number
// }
// export interface TestimonialModel {
// text: string,
// reviewer: ReviewerModel
// }

View File

@ -6,6 +6,15 @@ import { Collapse, Nav, NavItem, NavLink } from 'reactstrap'
import { FeatherIcon } from '../../../components/FeatherIcons'
import style from './scss/style.module.scss'
export interface ISideMenuItem {
icon?: string,
title?: string,
target?: string
childItems?: ISideMenuItem []
}
interface ISubMenu {
icon?: string,
title: string,

View File

@ -4,9 +4,15 @@ 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'
export interface INavMenuItem {
icon?: string,
title?: string,
target?: string
childItems?: INavMenuItem []
}
const NavMenu : FC = () => {
const { content, shopCart } = useSelector((state: ApplicationState) => state)
@ -33,7 +39,7 @@ const NavMenu : FC = () => {
<NavbarToggler onClick={toggle} className="mr-2"/>
<Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={state.isOpen} navbar>
<ul className="navbar-nav flex-grow">
{content?.topMenu ? content.topMenu.map((item: MenuItemModel, index: number) => {
{content?.topMenu ? content.topMenu.map((item: INavMenuItem, index: number) => {
return <NavItem key={index}>
<NavLink tag={Link} className="text-dark" to={item.target}><>
{item.icon ? <><FeatherIcon icon={item.icon}/> </> : ''}{titleFormatter(item.title)}</>

View File

@ -1,53 +0,0 @@
import { AuthorModel, FormItemModel, HeaderModel, ImageModel } from "./"
import { TitleSectionModel } from "./pageSections"
export interface Params {
[key: string]: string | undefined
}
export interface RequestModel<T, TT> {
pathParams?: T
searchParams?: TT,
}
export interface ResponseModel {}
export interface AddressPageSectionModel extends PageSectionModel {
firstName: FormItemModel,
lastName: FormItemModel,
address: FormItemModel,
address2: FormItemModel,
country: FormItemModel,
state: FormItemModel,
city: FormItemModel,
zip: FormItemModel
}
export interface PageModel {
header: HeaderModel,
titleSection?: TitleSectionModel
}
export interface PageSectionModel {
title?: string
text?: string
}
export interface PersonModel {
id: string,
image?: ImageModel
}
export interface PostItemModel {
id: string,
slug: string,
image: ImageModel,
badges: string [],
title: string,
shortText?: string,
text?: string,
author: AuthorModel,
created: string,
tags: string []
}

View File

@ -1,106 +0,0 @@
import { PersonModel, PostItemModel } from "./abstractions"
export interface PaginationModel<T> {
totalPages: number,
currentPage: number,
items: T []
}
export interface AuthorModel extends PersonModel {
nickName: string
}
export interface BlogItemModel extends PostItemModel {
readTime?: number,
likes?: number
}
export interface CategoryModel {
id: string,
text: string
}
export interface CommentModel {
author: AuthorModel,
comment: string,
responses?: CommentModel []
}
export interface FeatureModel {
icon: string,
title: string,
text: string
}
export interface FormItemModel {
title?: string,
optional?: string,
placeHolder?: string,
}
export interface HeaderLink {
[key: string]: string
}
export interface Meta {
[key: string]: string
}
export interface HeaderModel {
title: string,
link: HeaderLink,
meta: Meta
}
export interface ImageModel {
src: string,
alt: string
}
export interface LinkModel {
target: string,
anchorText: string
}
export interface LocalizationModel {
timeZone: string,
locale: string,
dateFormat: string,
timeFormat: string,
currency: string,
currencySymbol: 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,
brandName: string,
rating?: number,
price: number,
newPrice?: number,
quantity?: number
}
export interface TestimonialModel {
text: string,
reviewer: ReviewerModel
}

View File

@ -1,78 +0,0 @@
import { FeatureModel, FormItemModel, ImageModel, LinkModel, MenuItemModel, TestimonialModel } from "./"
import { AddressPageSectionModel, PageSectionModel } from "./abstractions"
export interface BillingAddressSectionModel extends AddressPageSectionModel { }
export interface CallToActionSectionModel extends PageSectionModel {
privacyDisclaimer?: string
email?: FormItemModel
}
export interface CartProductsSectionModel extends PageSectionModel {
product: string,
price: string,
quantity: string,
subtotal: string,
continueShopping: LinkModel,
checkout: LinkModel
}
export interface CheckoutSettingsSectionModel extends PageSectionModel {
shippingAddressSameAsBillingAddress: string,
saveThisInformation: string
}
export interface CheckoutSummarySectionModel extends PageSectionModel {
title: string,
total: string,
promoCode: FormItemModel,
submit: FormItemModel
}
export interface CommentsSectionModel extends PageSectionModel {
leaveComment: string
}
export interface FeaturedBlogSectionModel extends PageSectionModel {
readTime: string
}
export interface FeaturedBlogsSectionModel extends PageSectionModel {}
export interface FeaturesSectionModel extends PageSectionModel {
items: FeatureModel []
}
export interface PaymentSectionModel extends PageSectionModel {
nameOnCard: FormItemModel,
cardNumber: FormItemModel,
expiration: FormItemModel,
cvv: FormItemModel
}
export interface ProductSectionModel extends PageSectionModel {
availableQuantity: string,
addToCart: string
}
export interface RelatedProductsSectionModel extends PageSectionModel {
addToCart: string
}
export interface ShippingAddressSectionModel extends AddressPageSectionModel { }
export interface TestimonialsSectionModel extends PageSectionModel {
items: TestimonialModel []
}
export interface TitleSectionModel extends PageSectionModel {
image?: ImageModel,
primaryLink?: MenuItemModel,
secondaryLink?: MenuItemModel,
postedOnBy?: string
}
export interface ShopItemsSectionModel extends PageSectionModel {
addToCart: string
}

View File

@ -1,60 +0,0 @@
import { FormItemModel, LinkModel } from "."
import { PageModel } from "./abstractions"
import * as PageSection from "./pageSections"
export interface BlogCatalogPageModel extends PageModel {
featuredBlogSection: PageSection.FeaturedBlogSectionModel
}
export interface BlogItemPageModel extends PageModel {
commentsSection: PageSection.CommentsSectionModel
}
export interface HomePageModel extends PageModel {
featuresSection: PageSection.FeaturesSectionModel,
testimonialsSection: PageSection.TestimonialsSectionModel,
featuredBlogsSection: PageSection.FeaturedBlogsSectionModel,
callToActionSection: PageSection.CallToActionSectionModel
}
export interface ShopCartPageModel extends PageModel {
productsSection: PageSection.CartProductsSectionModel
}
export interface ShopCatalogPageModel extends PageModel {
shopItemsSection: PageSection.ShopItemsSectionModel
}
export interface ShopCheckoutPageModel extends PageModel {
billingAddressSection: PageSection.BillingAddressSectionModel,
shippingAddressSection: PageSection.ShippingAddressSectionModel,
settingsSection: PageSection.CheckoutSettingsSectionModel,
summarySection: PageSection.CheckoutSummarySectionModel,
paymentSection: PageSection.PaymentSectionModel,
submit: FormItemModel
}
export interface ShopItemPageModel extends PageModel {
productSection: PageSection.ProductSectionModel
relatedProductsSection: PageSection.RelatedProductsSectionModel
}
export interface SignInPageModel extends PageModel {
title: string,
email: FormItemModel,
password: FormItemModel,
dontHaveAnAccount: string,
signUpLink: LinkModel,
submit: FormItemModel
}
export interface SignUpPageModel extends PageModel {
title: string,
username: FormItemModel,
email: FormItemModel,
reEmail: FormItemModel
password: FormItemModel,
rePassword: FormItemModel,
acceptTermsAndConditions: string,
submit: FormItemModel
}

View File

@ -1,107 +0,0 @@
import { Params, RequestModel } from "./abstractions"
// Blog requests -----------------------------------------------------
export interface GetBlogcatalogPathParams extends Params {}
export interface GeBlogCatalogSearchParams extends Params {}
export interface GetBlogCatalogRequestModel extends RequestModel<GetBlogcatalogPathParams, GeBlogCatalogSearchParams> {
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
// --------------------------------------------------------------------
export interface GetBlogCategoriesPathParams extends Params {}
export interface GetBlogCategoriesSearchParams extends Params {}
export interface GetBlogCategoriesRequestModel extends RequestModel<GetBlogCategoriesPathParams, GetBlogCategoriesSearchParams> { }
// --------------------------------------------------------------------
export interface GetBlogItemPathParams extends Params {}
export interface GetBlogItemSearchParams extends Params {
slug: string
}
export interface GetBlogItemRequestModel extends RequestModel<GetBlogItemPathParams, GetBlogItemSearchParams> {
}
// ---------------------------------------------------------------------
export interface GetBlogFeaturedPathParams extends Params {}
export interface GetBlogFeaturedSearchParams extends Params {}
export interface GetBlogFeaturedRequestModel extends RequestModel<GetBlogFeaturedPathParams, GetBlogFeaturedSearchParams> { }
// Static content -------------------------------------------------
export interface GetContentPathParams extends Params {}
export interface GetContentSearchParams extends Params {
locale?: string
}
export interface GetContentRequestModel extends RequestModel<GetContentPathParams, GetContentSearchParams> { }
// Shop requests -------------------------------------------------
export interface GetShopCatalogPathParams extends Params {}
export interface GetShopCatalogSearchParams extends Params {
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
export interface GetShopCatalogRequestModel extends RequestModel<GetShopCatalogPathParams, GetShopCatalogSearchParams> { }
// Shop cart items ------------------------------------------------
export interface GetShopCartItemsPathParams extends Params {
userId: string
}
export interface GetShopCartItemsSearchParams extends Params {
userId: string
}
export interface GetShopCartItemsRequestModel extends RequestModel<GetShopCartItemsPathParams, GetShopCartItemsSearchParams> {}
// -------------------------------------------------------------------
export interface GetShopCategoriesPathParams extends Params {}
export interface GetShopCategoriesSearchParams extends Params {}
export interface GetShopCategoriesRequestModel extends RequestModel<GetShopCategoriesPathParams, GetShopCategoriesSearchParams> { }
// ------------------------------------------------------------------
export interface GetShopFeaturedSearchParams extends Params {}
export interface GetShopFeaturedPathParams extends Params {}
export interface GetShopFeaturedRequestModel extends RequestModel<GetShopFeaturedSearchParams, GetShopFeaturedPathParams> { }
// -------------------------------------------------------------------
export interface GetShopItemPathParams extends Params {}
export interface GetShopItemSearchParams extends Params {
slug: string
}
export interface GetShopItemRequestModel extends RequestModel<GetShopItemPathParams, GetShopItemSearchParams> {
}
// -------------------------------------------------------------------
export interface GetShopRelatedPathParams extends Params {}
export interface GetShopRelatedSearchParams extends Params {}
export interface GetShopRelatedRequestModel extends RequestModel<GetShopRelatedPathParams, GetShopRelatedSearchParams> { }
// -------------------------------------------------------------------
export interface GetShopCartItemPathParams extends Params {}
export interface GetShopCartItemSearchParams extends Params {}
export interface GetShopCartItemRequestModel extends RequestModel<GetShopCartItemPathParams, GetShopCartItemSearchParams> {}

View File

@ -1,93 +0,0 @@
import { BlogItemModel, CategoryModel, CommentModel, HeaderModel, ImageModel, LocalizationModel, MenuItemModel, PaginationModel, RouteModel, ShopItemModel } from "./"
import { ResponseModel } from "./abstractions"
import * as Pages from "./pages"
// Shop response models
export interface GetShopCatalogResponseModel extends PaginationModel<ShopItemModel>, ResponseModel {}
export interface GetShopCategoriesResponseModel extends ResponseModel {
items: CategoryModel []
}
export interface GetShopFeaturedResponseModel extends ResponseModel {
items: ShopItemModel []
}
export interface GetShopItemResponseModel extends ShopItemModel, ResponseModel {
comments: CommentModel []
}
export interface GetShopRelatedResponseModel extends ResponseModel {
items: ShopItemModel []
}
export interface GetShopCartItemResponseModel {
slug: string
sku: string,
image: ImageModel,
title: string,
brandName: string,
shortText: string,
created: string,
price: number,
newPrice?: number,
quantity: number
}
// Static content response model
export interface GetContentResponseModel extends ResponseModel {
siteName: string,
siteUrl: string,
header: HeaderModel,
localization: LocalizationModel,
routes: RouteModel [],
adminRoutes: RouteModel [],
serviceRoutes: RouteModel [],
topMenu: MenuItemModel [],
sideMenu: MenuItemModel [],
homePage: Pages.HomePageModel,
shopCatalog: Pages.ShopCatalogPageModel,
shopItem: Pages.ShopItemPageModel,
shopCart: Pages.ShopCartPageModel,
shopCheckout: Pages.ShopCheckoutPageModel,
blogCatalog: Pages.BlogCatalogPageModel,
blogItem: Pages.BlogItemPageModel,
signIn: Pages.SignInPageModel,
signUp: Pages.SignUpPageModel
}
// Blog response models
export interface GetBlogCatalogResponseModel extends PaginationModel<BlogItemModel>, ResponseModel { }
export interface GetBlogCategoriesResponseModel extends ResponseModel {
items: CategoryModel []
}
export interface GetBlogFeaturedResponseModel extends ResponseModel {
items: BlogItemModel []
}
export interface GetBlogItemResponseModel extends BlogItemModel, ResponseModel {
comments: CommentModel []
}
// Weather forecasts
export interface GetWeatherForecastResponseModel extends ResponseModel {
date: string,
temperatireC: number,
temperatureF: number,
summary?: string
}

View File

@ -1,6 +1,10 @@
import * as React from 'react'
import React, { FC } from 'react'
const AdminHome = () => {
interface IAdminHomeComponent {
}
const AdminHome : FC<IAdminHomeComponent> = () => {
return <div>Admin Home</div>
}

View File

@ -0,0 +1,108 @@
// React
import React, { FC } from 'react'
import { Link, useNavigate } from 'react-router-dom'
// Redux
import { useDispatch } from 'react-redux'
// Reducers
import { actionCreators as blogCatalogActionCreators } from '../../../store/reducers/BlogCatalog'
// Reactstrap
import { Card, CardBody, CardImg, Col } from 'reactstrap'
// Components
import { ISSRPaginationComponent, SSRPagination } from '../../../components/Pagination'
// Functions
import { dateFormat } from '../../../functions'
interface IImage {
src: string,
alt: string
}
interface IAuthor {
id: string,
image?: IImage
nickName: string
}
export interface IBlogItem {
id: string,
slug: string,
image: IImage,
badges: string [],
title: string,
shortText?: string,
text?: string,
author: IAuthor,
created: string,
tags: string []
readTime?: number,
likes?: number
}
export interface IBlogItems {
totalPages?: number,
currentPage?: number,
items?: IBlogItem []
}
export interface IBlogItemsSection {
readMore: string
}
export interface IBlogItemsComponent extends IBlogItemsSection, IBlogItems {
path: string
}
const BlogItemsComponent: FC<IBlogItemsComponent> = (props) => {
const { path, totalPages, currentPage, items = [], readMore } = props
const dispatch = useDispatch()
const navigate = useNavigate()
return <>
{items.map((item, index) => <Col key={index} className="lg-6 mb-3">
<Card>
<CardImg top {...item.image} />
<CardBody>
{item.badges.map((badge, index) => <div key={index} className="badge bg-primary bg-gradient rounded-pill mb-2">{badge}</div>)}
<div className="small text-muted">{dateFormat(item.created)}</div>
<h2 className="card-title h4">{item.title}</h2>
<p className="card-text">{item.shortText}</p>
<Link to={`${path}/${currentPage}/${item.slug}`} className="btn btn-primary">{readMore}</Link>
</CardBody>
</Card>
</Col>)}
<SSRPagination {...{
totalPages: totalPages,
currentPage: currentPage,
// onClick: (nextPage) => {
// dispatch(blogCatalogActionCreators.requestBlogCatalog({
// searchParams: {
// currentPage: nextPage + ""
// }
// }))
// navigate(`${path}/${nextPage}`)
// }
linksPath: path
} as ISSRPaginationComponent} />
</>
}
export {
BlogItemsComponent
}

View File

@ -0,0 +1,82 @@
import React, { FC } from 'react'
import { Link } from 'react-router-dom'
import { Card, CardBody, CardFooter, CardImg } from 'reactstrap'
import { dateFormat } from '../../../functions'
interface IImage {
src: string,
alt: string
}
interface IAuthor {
id: string,
image?: IImage
nickName: string
}
export interface IFeaturedBlogItem {
id: string,
slug: string,
image: IImage,
badges: string [],
title: string,
shortText: string,
author: IAuthor,
created: string,
tags: string []
readTime?: number,
likes?: number
}
// static stuff from Content
export interface IFeaturedBlogSection {
readTime?: string
}
// component props
export interface IFeaturedBlogComponent extends IFeaturedBlogSection {
path: string,
currentPage: number
item: IFeaturedBlogItem,
}
const FeaturedBlogComponent: FC<IFeaturedBlogComponent> = (props) => {
const { currentPage, path, item } = props
let { readTime } = props
if(readTime && item?.created && item?.readTime)
readTime = readTime?.replace('{date}', dateFormat(item.created))
.replace('{readTime}', `${item.readTime}`)
return <Card className="mb-4 shadow border-0">
<CardImg top {...item.image} />
<CardBody className="p-4">
{item.badges.map((badge, index) => <div key={index} className="badge bg-primary bg-gradient rounded-pill mb-2">{badge}</div>)}
<div className="small text-muted">{dateFormat(item.created)}</div>
<Link className="text-decoration-none link-dark stretched-link" to={`${path}/${currentPage}/${item.slug}`}>
<h5 className="card-title mb-3">{item.title}</h5>
</Link>
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: item.shortText }}></p>
</CardBody>
<CardFooter className="p-4 pt-0 bg-transparent border-top-0">
<div className="d-flex align-items-end justify-content-between">
<div className="d-flex align-items-center">
<img className="rounded-circle me-3" {...item.author.image} />
<div className="small">
<div className="fw-bold">{item.author.nickName}</div>
{readTime !=null ? <div className="text-muted">{readTime}</div> : <></>}
</div>
</div>
</div>
</CardFooter>
</Card>
}
export {
FeaturedBlogComponent
}

View File

@ -0,0 +1,30 @@
// React
import React, { FC } from 'react'
// Reactstrap
import { Container } from 'reactstrap'
export interface ITitleSection {
title: string,
text: string
}
export interface ITitleComponent extends ITitleSection {}
const TitleSection: FC<ITitleComponent> = (props) => {
const { title, text } = props
return <header className="py-5 bg-light border-bottom mb-4">
<Container fluid>
<div className="text-center my-5">
<h1 className="display-4 fw-bolder">{title}</h1>
<p className="lead fw-normal text-50 mb-0">{text}</p>
</div>
</Container>
</header>
}
export {
TitleSection
}

View File

@ -1,195 +1,103 @@
// React
import React, { FC, useEffect } from 'react'
import { useLocation, useParams } from 'react-router-dom'
// Redux
import { useSelector, useDispatch } from 'react-redux'
import { ApplicationState } from '../../../store'
import { actionCreators as contentActionCreators } from '../../../store/reducers/Content'
import { actionCreators as loaderActionCreators } from '../../../store/reducers/Loader'
// Reducers
import { actionCreators as blogCatalogActionCreators } from '../../../store/reducers/BlogCatalog'
import { actionCreators as blogFeaturedActionCreators } from '../../../store/reducers/BlogFeatured'
import { actionCreators as blogCategoriesActionCreators } from '../../../store/reducers/BlogCategories'
import { actionCreators as blogFeaturedActionCreators } from '../../../store/reducers/BlogFeatured'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
import { dateFormat, findRoutes } from '../../../functions'
// Reactstrap
import { Col, Container, Row } from 'reactstrap'
// Components
import { TitleSection, ITitleSection } from './TitleComponent'
import { FeaturedBlogComponent, IFeaturedBlogComponent, IFeaturedBlogSection } from './FeaturedBlogComponent'
import { BlogItemsComponent, IBlogItemsComponent, IBlogItemsSection } from './BlogItemsComponent'
import { Categories, Empty, Search } from '../../../components/SideWidgets'
import { Pagination } from '../../../components/Pagination'
import { BlogItemModel } from '../../../models'
import { TitleSectionModel } from '../../../models/pageSections'
// Interfaces
import { IHeader } from '../../../interfaces'
import { cloneObject } from '../../../functions'
const TitleSection: FC<TitleSectionModel> = (props) => {
const { title, text } = props
return <header className="py-5 bg-light border-bottom mb-4">
<Container fluid>
<div className="text-center my-5">
<h1 className="fw-bolder">{title ? title : ''}</h1>
<p className="lead mb-0">{text ? text : ''}</p>
</div>
</Container>
</header>
export interface IBlogCatalogPage {
header: IHeader,
titleSection: ITitleSection,
blogItemsSection: IBlogItemsSection,
featuredBlogSection: IFeaturedBlogSection
}
interface FeaturedBlog {
path?: string,
currentPage?: number
item?: BlogItemModel,
readTime?: string
}
export interface IBlogCatalogComponent extends IBlogCatalogPage { }
const FeaturedBlogSection: FC<FeaturedBlog> = ({
currentPage = 1,
path = "",
item = {
id: "",
slug: "demo-post",
badges: [],
image: {
src: `${process.env.REACT_APP_API}/Image/850x350/dee2e6/6c757d`,
alt: "..."
},
title: "",
shortText: "",
author: {
id: "",
nickName: "",
image: {
src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`,
alt: "..."
}
},
created: new Date().toString(),
tags: [],
likes: 0
},
readTime = ""
}) => {
return <Card className="mb-4 shadow border-0">
<CardImg top {...item.image} />
<CardBody className="p-4">
{item.badges.map((badge, index) => <div key={index} className="badge bg-primary bg-gradient rounded-pill mb-2">{badge}</div>)}
<Link className="text-decoration-none link-dark stretched-link" to={`${path}/${currentPage}/${item.slug}`}>
<h5 className="card-title mb-3">{item.title}</h5>
</Link>
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: item.shortText ? item.shortText : '' }}></p>
</CardBody>
<CardFooter className="p-4 pt-0 bg-transparent border-top-0">
<div className="d-flex align-items-end justify-content-between">
<div className="d-flex align-items-center">
<img className="rounded-circle me-3" {...item.author.image} />
<div className="small">
<div className="fw-bold">{item.author.nickName}</div>
<div className="text-muted">{readTime}</div>
</div>
</div>
</div>
</CardFooter>
</Card>
}
interface BlogItems {
path?: string
totalPages?: number,
currentPage?: number,
items?: BlogItemModel []
}
const BlogItemsSection: FC<BlogItems> = ({
path = "",
totalPages = 1,
currentPage = 1,
items = []
}) => {
const dispatch = useDispatch()
const navigate = useNavigate()
return <>
{items.map((item, index) => <Col key={index} className="lg-6">
<Card className="mb-4">
<CardImg top {...item.image} />
<CardBody>
<div className="small text-muted">{dateFormat(item.created)}</div>
<h2 className="card-title h4">{item.title}</h2>
<p className="card-text">{item.shortText}</p>
<Link to={`${path}/${currentPage}/${item.slug}`} className="btn btn-primary">Read more </Link>
</CardBody>
</Card>
</Col>)}
<Pagination {...{
totalPages: totalPages,
currentPage: currentPage,
onClick: (nextPage) => {
dispatch(blogCatalogActionCreators.requestBlogCatalog({
currentPage: nextPage + ""
}))
navigate(`${path}/${nextPage}`)
}
}} />
</>
}
const BlogCatalog = () => {
const BlogCatalog : FC<IBlogCatalogComponent> = () => {
const location = useLocation()
const params = useParams()
const dispatch = useDispatch()
const { content, blogCatalog, blogCategories, blogFeatured } = useSelector((state: ApplicationState) => state)
const page = content?.blogCatalog
const path = findRoutes(content?.routes, 'BlogCatalog')[0]?.targets[0]
const { dateFormat, timeFormat } = content.localization
const { header, titleSection, blogItemsSection, featuredBlogSection } = content.blogCatalog
useEffect(() => {
dispatch(blogCatalogActionCreators.requestBlogCatalog({
currentPage: params?.page ? params.page : "1"
}))
dispatch(blogFeaturedActionCreators.requestBlogFeatured())
dispatch(blogCategoriesActionCreators.requestBlogCategories())
}, [])
useEffect(() => {
blogCatalog?.isLoading
? dispatch(loaderActionCreators.show())
: setTimeout(() => {
dispatch(loaderActionCreators.hide())
}, 1000)
}, [blogCatalog?.isLoading])
dispatch(blogCatalogActionCreators.requestBlogCatalog({
searchParams: {
category: params?.category,
currentPage: params?.page
}
}))
}, [params.category, params.page])
const updateBlogCategories = () => {
const newBlogCategoies = cloneObject(blogCategories)
newBlogCategoies.items = newBlogCategoies.items.map(item => {
item.href = `${location.pathname.split('/').slice(0, 2).join('/')}/${item.href}`
return item
})
return newBlogCategoies
}
const updateBlogLinks = () => {
return location.pathname.split('/').slice(0, 3).join('/')
}
const blogItem = blogFeatured?.items[0]
const featuredBlog: FeaturedBlog = {
path,
currentPage: blogCatalog?.currentPage,
item: blogItem,
readTime: page?.featuredBlogSection?.readTime
}
if(featuredBlog.readTime && blogItem?.created && blogItem?.readTime)
featuredBlog.readTime = featuredBlog.readTime?.replace('{date}', dateFormat(blogItem.created))
.replace('{readTime}', `${blogItem.readTime}`)
return <>
<TitleSection {...page?.titleSection} />
<TitleSection {...titleSection} />
<Container fluid>
<Row>
<Col>
<FeaturedBlogSection {...featuredBlog} />
<FeaturedBlogComponent {...{
path: location.pathname,
currentPage: blogCatalog?.currentPage,
item: blogItem,
featuredBlogSection
} as IFeaturedBlogComponent} />
<Row>
<BlogItemsSection path={path} {...blogCatalog} />
<BlogItemsComponent {...{
path: updateBlogLinks(),
...blogCatalog,
...blogItemsSection
} as IBlogItemsComponent }/>
</Row>
</Col>
<Col lg="4">
{/*<Search />
<Categories {...blogCategories} />*/}
<Col lg="3">
<Search />
<Categories {...updateBlogCategories()} />
<Empty/>
</Col>
</Row>

View File

@ -1,25 +1,48 @@
import React, { FC } from 'react'
import { Card, CardBody } from 'reactstrap'
import { CommentModel } from '../../models'
import { CommentsSectionModel } from '../../models/pageSections'
interface Comments {
staticContent?: CommentsSectionModel,
items?: CommentModel []
interface IImage {
src: string,
alt: string
}
const CommentsSection: FC<Comments> = ({
staticContent,
items = []
}) => {
interface IAuthor {
id: string,
image?: IImage
nickName: string
}
export interface IComment {
author: IAuthor,
comment: string,
responses?: IComment []
}
export interface IComments {
path?: string
totalPages?: number,
currentPage?: number,
items?: IComment []
}
export interface ICommentsSection {
leaveComment: string
}
export interface ICommentsComponent extends ICommentsSection, IComments { }
const CommentsComponent: FC<ICommentsComponent> = (props) => {
const { leaveComment, items = [] } = props
return <section className="mb-5">
<Card className="card bg-light">
<CardBody className="card-body">
<form className="mb-4">
<textarea className="form-control" rows={3} placeholder={staticContent?.leaveComment}></textarea>
<textarea className="form-control" rows={3} placeholder={leaveComment}></textarea>
</form>
{items.map((comment, index) => <div key={index} className={`d-flex ${index < items.length - 1 ? 'mb-4' : ''}`}>
@ -50,5 +73,5 @@ const CommentsSection: FC<Comments> = ({
}
export {
CommentsSection
CommentsComponent
}

View File

@ -0,0 +1,40 @@
// React
import React, { FC } from 'react'
interface IImage {
src: string,
alt: string
}
export interface ITitleSection {
text :string,
}
export interface ITitleComponent extends ITitleSection {
title: string,
badges? : string[],
image?: IImage
}
const TitleSection: FC<ITitleComponent> = (props) => {
const { title, text, badges = [], image } = props
return <>
<header className="mb-4">
<h1 className="fw-bolder mb-1">{title}</h1>
<div className="text-muted fst-italic mb-2">{text ? text : ''}</div>
{badges ? badges.map((badge, index) => <a key={index} className="badge bg-secondary text-decoration-none link-light" href="#!">{badge}</a>) : <></>}
</header>
<figure className="mb-4">
{image != null ? <img className="img-fluid rounded" {...image} /> : <></>}
</figure>
</>
}
export {
TitleSection
}

View File

@ -4,57 +4,63 @@ import { useParams } from 'react-router-dom'
// Redux
import { useDispatch, useSelector } from 'react-redux'
import { actionCreators as loaderActionCreators } from '../../../store/reducers/Loader'
import { actionCreators as blogItemActionCreators } from '../../../store/reducers/BlogItem'
import { Col, Container, Row } from 'reactstrap'
import { CommentsSection } from '../../../components/Comments'
import { Categories, Empty, Search } from '../../../components/SideWidgets'
import { ApplicationState } from '../../../store'
import { TitleSection, ITitleSection, ITitleComponent } from './TitleSection'
import { CommentsComponent, ICommentsSection, ICommentsComponent } from './CommentsComponent'
import { IHeader } from '../../../interfaces'
import { dateFormat } from '../../../functions'
import { ImageModel } from '../../../models'
interface BlogItemTitle {
title?: string,
postedOnBy? :string,
badges? : string[],
image?: ImageModel
interface IImage {
src: string,
alt: string
}
const BlogTitleSection: FC<BlogItemTitle> = ({
title = "",
postedOnBy = "",
badges = [],
image = {
src: "",
alt: ""
}
}) => {
interface IAuthor {
id: string,
image?: IImage
return <>
<header className="mb-4">
<h1 className="fw-bolder mb-1">{title}</h1>
<div className="text-muted fst-italic mb-2">{postedOnBy ? postedOnBy : ''}</div>
nickName: string
}
export interface IBlogItem {
id: string,
slug: string,
image: IImage,
badges: string [],
title: string,
shortText?: string,
text?: string,
author: IAuthor,
created: string,
tags: string []
{badges ? badges.map((badge, index) => <a key={index} className="badge bg-secondary text-decoration-none link-light" href="#!">{badge}</a>) : <></>}
</header>
<figure className="mb-4">
<img className="img-fluid rounded" {...image} />
</figure>
</>
readTime?: number,
likes?: number
}
const BlogItem = () => {
export interface IBlogItemPage {
header: IHeader,
titleSection: ITitleSection,
commentsSection: ICommentsSection
}
export interface IBlogItemComponent extends IBlogItemPage {}
const BlogItem : FC<IBlogItemComponent> = () => {
const params = useParams()
const dispatch = useDispatch()
const { content, blogItem } = useSelector((state: ApplicationState) => state)
const page = content?.blogItem
const { content, blogItem, comments } = useSelector((state: ApplicationState) => state)
const { titleSection, commentsSection } = content.blogItem
useEffect(() => {
if(params?.slug)
@ -65,40 +71,32 @@ const BlogItem = () => {
}))
}, [])
useEffect(() => {
blogItem?.isLoading
? dispatch(loaderActionCreators.show())
: setTimeout(() => {
dispatch(loaderActionCreators.hide())
}, 1000)
}, [blogItem?.isLoading])
const blogItemTitle: BlogItemTitle = {
const blogItemTitle: ITitleComponent = {
title: blogItem?.title,
postedOnBy: page?.titleSection?.postedOnBy,
text: titleSection?.text,
badges: blogItem?.badges,
image: blogItem?.image
}
if(blogItemTitle.postedOnBy && blogItem?.created)
blogItemTitle.postedOnBy = blogItemTitle.postedOnBy?.replace('{date}', dateFormat(blogItem.created))
if(blogItemTitle.text && blogItem?.created)
blogItemTitle.text = blogItemTitle.text?.replace('{date}', dateFormat(blogItem.created))
if(blogItemTitle.postedOnBy && blogItem?.author)
blogItemTitle.postedOnBy = blogItemTitle.postedOnBy?.replace('{nickName}', dateFormat(blogItem.author.nickName))
if(blogItemTitle.text && blogItem?.author)
blogItemTitle.text = blogItemTitle.text?.replace('{nickName}', dateFormat(blogItem.author.nickName))
return <Container fluid className="mt-5">
<Row>
<Col lg="8">
<article>
<BlogTitleSection {...blogItemTitle} />
<TitleSection {...blogItemTitle} />
<section className="mb-5" dangerouslySetInnerHTML={{ __html: blogItem?.text ? blogItem.text : '' }}></section>
</article>
<CommentsSection {...{
staticContent: page?.commentsSection,
items: blogItem?.comments
}} />
<CommentsComponent {...{
...commentsSection,
...comments
} as ICommentsComponent} />
</Col>
<Col lg="4">

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, { FC } from 'react'
// Redux
import { useDispatch, useSelector } from 'react-redux'
@ -8,7 +8,9 @@ interface IReduxState {
counter: CounterState
}
const Counter = () => {
export interface ICounterComponent {}
const Counter : FC<ICounterComponent> = () => {
const dispatch = useDispatch()
const counterState = useSelector((state: IReduxState) => state.counter)

View File

@ -1,5 +1,5 @@
// React
import React, { useEffect } from 'react'
import React, { FC, useEffect } from 'react'
import { Link, useLocation, useParams } from 'react-router-dom'
// Redux
@ -15,7 +15,9 @@ type IParams = {
startDateIndex: string
}
const FetchData = () => {
export interface IFetchDataComponent {}
const FetchData : FC<IFetchDataComponent> = () => {
const location = useLocation()
const params = useParams<IParams>()

View File

@ -0,0 +1,43 @@
import React, { FC } from "react"
import { Col, Container, Row } from "reactstrap"
interface IEmail {
title: string,
placeHolder: string
}
export interface ICallToActionSection {
title: string,
text: string,
privacyDisclaimer: string,
email: IEmail
}
const CallToActionSection: FC<ICallToActionSection> = (props) => {
const { title, text, privacyDisclaimer, email } = props
return <section className="py-5">
<Container fluid className="px-5 my-5">
<aside className="bg-primary bg-gradient rounded-3 p-4 p-sm-5 mt-5">
<div className="d-flex align-items-center justify-content-between flex-column flex-xl-row text-center text-xl-start">
<div className="mb-4 mb-xl-0">
<div className="fs-3 fw-bold text-white">{title}</div>
<div className="text-white-50">{text}</div>
</div>
<div className="ms-xl-4">
<div className="input-group mb-2">
<input className="form-control" type="text" placeholder={email.placeHolder} aria-label={email.placeHolder} aria-describedby="button-newsletter" />
<button className="btn btn-outline-light" id="button-newsletter" type="button">{email.title ? email.title : ''}</button>
</div>
<div className="small text-white-50">{privacyDisclaimer}</div>
</div>
</div>
</aside>
</Container>
</section>
}
export {
CallToActionSection
}

View File

@ -0,0 +1,100 @@
// React
import React, { FC } from 'react'
import { Link } from 'react-router-dom'
// Reactstrap
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
import { dateFormat } from '../../functions'
interface IImage {
src: string,
alt: string
}
interface IAuthor {
id: string,
image?: IImage,
nickName: string
}
export interface IFeaturedBlogItem {
id: string,
slug: string,
image: IImage,
badges: string [],
title: string,
shortText: string,
author: IAuthor,
created: string,
tags: string []
readTime: number,
likes?: number
}
export interface IFeaturedBlogsSection {
title: string,
text: string,
readTime: string
}
export interface IFeaturedBlogsFull extends IFeaturedBlogsSection {
items?: IFeaturedBlogItem []
}
const FeaturedBlogsSection: FC<IFeaturedBlogsFull> = (props) => {
const { title, text, items = [] } = props
const readTimeString = (itemCreated: string, itemReadTime: number) : string => {
let { readTime } = props
if(readTime && itemCreated && itemReadTime)
readTime = readTime?.replace('{date}', dateFormat(itemCreated))
.replace('{readTime}', `${itemReadTime}`)
return readTime
}
return <section className="py-5">
<Container fluid className="px-5 my-5">
<Row className="gx-5 justify-content-center">
<Col className="lg-8 xl-6">
<div className="text-center">
<h2 className="fw-bolder">{title}</h2>
<p className="lead fw-normal text-muted mb-5" dangerouslySetInnerHTML={{ __html: text }}></p>
</div>
</Col>
</Row>
<Row className="gx-5">
{items.map((item, index) => <Col key={index} className="lg-4 mb-5">
<Card className="h-100 shadow border-0">
<CardImg top {...item.image} />
<CardBody className="p-4">
<div className="badge bg-primary bg-gradient rounded-pill mb-2">{item.badges}</div>
<Link className="text-decoration-none link-dark stretched-link" to="#!">
<h5 className="card-title mb-3">{item.title}</h5>
</Link>
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: item.shortText }}></p>
</CardBody>
<CardFooter className="p-4 pt-0 bg-transparent border-top-0">
<div className="d-flex align-items-end justify-content-between">
<div className="d-flex align-items-center">
<img className="rounded-circle me-3" {...item.author.image} />
<div className="small">
<div className="fw-bold">{item.author.nickName}</div>
<div className="text-muted" dangerouslySetInnerHTML={{ __html: readTimeString(item.created, item.readTime)}}></div>
</div>
</div>
</div>
</CardFooter>
</Card>
</Col>)}
</Row>
</Container>
</section>
}
export {
FeaturedBlogsSection
}

View File

@ -0,0 +1,50 @@
// React
import React, { FC } from 'react'
// Reactstrap
import { Col, Container, Row } from 'reactstrap'
import { FeatherIcon } from '../../components/FeatherIcons'
//
import style from './scss/style.module.scss'
interface IFeatureItem {
icon: string,
title: string,
text: string
}
export interface IFeaturesSection {
title: string,
items?: IFeatureItem []
}
const FeaturesSection: FC<IFeaturesSection> = (props) => {
const { title, items = [] } = props
return <section className="py-5" id="features">
<Container fluid className="px-5 my-5">
<Row className="gx-5">
<Col className="lg-4 mb-5 mb-lg-0">
<h2 className="fw-bolder mb-0">{title}</h2>
</Col>
<Col className="lg-8">
<Row className="gx-5 cols-1 cols-md-2">
{items ? items.map((item, index) => <Col key={index} className="mb-5 h-100">
<div className={`${style.feature} bg-primary bg-gradient text-white rounded-3 mb-3`}>
<FeatherIcon icon={item.icon} />
</div>
<h2 className="h5">{item.title}</h2>
<p className="mb-0" dangerouslySetInnerHTML={{ __html: item.text }}></p>
</Col>) : ''}
</Row>
</Col>
</Row>
</Container>
</section>
}
export {
FeaturesSection
}

View File

@ -0,0 +1,47 @@
import React, { FC } from "react"
import { Col, Container, Row } from "reactstrap"
interface IImage {
src: string
alt?: string
}
interface IReviewer {
image: IImage,
fullName: string,
position: string
}
interface ITestimonialItem {
text: string,
reviewer: IReviewer
}
export interface ITestimonialsSection {
items: ITestimonialItem []
}
const TestimonialsSection: FC<ITestimonialsSection> = (props) => {
const item = props.items[0]
return <section className="py-5 bg-light">
<Container fluid className="px-5 my-5">
<Row className="gx-5 justify-content-center">
<Col className="lg-10 xl-7">
<div className="text-center">
<div className="fs-4 mb-4 fst-italic" dangerouslySetInnerHTML={{ __html: item.text }}></div>
<div className="d-flex align-items-center justify-content-center">
<img className="rounded-circle me-3" {...item.reviewer.image} />
<div className="fw-bold">{item.reviewer.fullName}<span className="fw-bold text-primary mx-1">/</span>{item.reviewer.position}
</div>
</div>
</div>
</Col>
</Row>
</Container>
</section>
}
export {
TestimonialsSection
}

View File

@ -0,0 +1,36 @@
import React, { FC } from "react"
import { Col, Container, Row } from "reactstrap"
export interface ITitleSection {
title: string,
text: string
}
const TitleSection : FC<ITitleSection> = (props) => {
const { title, text } = props
return <header className="py-5 bg-dark">
<Container fluid className="px-5">
<Row className="gx-5 align-items-center justify-content-center">
<Col className="lg-8 xl-7 xxl-6">
<div className="my-5 text-center text-xl-start">
<h1 className="display-5 fw-bolder text-white mb-2">{title}</h1>
<span className="lead fw-normal text-white-50 mb-4" dangerouslySetInnerHTML={{ __html: text }}>
</span>
<div className="d-grid gap-3 d-sm-flex justify-content-sm-center justify-content-xl-start">
<a className="btn btn-primary btn-lg px-4 me-sm-3" href="#features">Get Started</a>
<a className="btn btn-outline-light btn-lg px-4" href="#!">Learn More</a>
</div>
</div>
</Col>
<div className="col-xl-5 col-xxl-6 d-none d-xl-block text-center"><img className="img-fluid rounded-3 my-5" src={`${process.env.REACT_APP_FRONTEND}/Image/600x400/343a40/6c757d`} alt="..." /></div>
</Row>
</Container>
</header>
}
export {
TitleSection
}

View File

@ -1,240 +1,61 @@
// React
import React, { FC, useEffect } from 'react'
import { Link } 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 blogFeaturedActionCreators } from '../../store/reducers/BlogFeatured'
import { actionCreators as headerActionCreators } from '../../store/reducers/Header'
// Reactstrap
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
// Models (interfaces)
import { CallToActionSectionModel, FeaturedBlogsSectionModel, FeaturesSectionModel, TestimonialsSectionModel, TitleSectionModel } from '../../models/pageSections'
import { BlogItemModel, FeatureModel, HeaderModel, TestimonialModel } from '../../models'
// Custom components
import { FeatherIcon } from '../../components/FeatherIcons'
import { IHeader } from '../../interfaces'
import { TitleSection, ITitleSection } from './TitleSection'
import { FeaturesSection, IFeaturesSection } from './FeaturesSection'
import { TestimonialsSection, ITestimonialsSection } from './TestimonialsSection'
import { FeaturedBlogsSection, IFeaturedBlogsFull, IFeaturedBlogsSection } from './FeaturedBlogsSection'
import { CallToActionSection, ICallToActionSection } from './CallToActionSection'
// Functions
import { dateFormat } from '../../functions'
// CSS Modules
import style from './scss/style.module.scss'
const TitleSection : FC<TitleSectionModel> = ({
title = "",
text = ""
}) => {
return <header className="py-5 bg-dark">
<Container fluid className="px-5">
<Row className="gx-5 align-items-center justify-content-center">
<Col className="lg-8 xl-7 xxl-6">
<div className="my-5 text-center text-xl-start">
<h1 className="display-5 fw-bolder text-white mb-2">{title}</h1>
<span className="lead fw-normal text-white-50 mb-4" dangerouslySetInnerHTML={{ __html: text }}>
</span>
<div className="d-grid gap-3 d-sm-flex justify-content-sm-center justify-content-xl-start">
<a className="btn btn-primary btn-lg px-4 me-sm-3" href="#features">Get Started</a>
<a className="btn btn-outline-light btn-lg px-4" href="#!">Learn More</a>
</div>
</div>
</Col>
<div className="col-xl-5 col-xxl-6 d-none d-xl-block text-center"><img className="img-fluid rounded-3 my-5" src={`${process.env.REACT_APP_API}/Image/600x400/343a40/6c757d`} alt="..." /></div>
</Row>
</Container>
</header>
export interface IHomePage {
header: IHeader,
titleSection: ITitleSection,
featuresSection: IFeaturesSection,
testimonialsSection: ITestimonialsSection,
featuredBlogsSection: IFeaturedBlogsSection,
callToActionSection: ICallToActionSection
}
interface Fetures {
title?: string,
items?: FeatureModel []
}
export interface IHomePageComponent extends IHomePage { }
const FeaturesSection: FC<Fetures> = ({
title = "",
items = []
}) => {
return <section className="py-5" id="features">
<Container fluid className="px-5 my-5">
<Row className="gx-5">
<Col className="lg-4 mb-5 mb-lg-0">
<h2 className="fw-bolder mb-0">{title ? title : ''}</h2>
</Col>
<Col className="lg-8">
<Row className="gx-5 cols-1 cols-md-2">
{items ? items.map((item, index) => <Col key={index} className="mb-5 h-100">
<div className={`${style.feature} bg-primary bg-gradient text-white rounded-3 mb-3`}>
<FeatherIcon icon={item.icon} />
</div>
<h2 className="h5">{item.title}</h2>
<p className="mb-0" dangerouslySetInnerHTML={{ __html: item.text }}></p>
</Col>) : ''}
</Row>
</Col>
</Row>
</Container>
</section>
}
interface Testimonials {
items?: TestimonialModel []
}
const TestimonialsSection: FC<Testimonials> = ({
items = []
}) => {
const item = items[0]
return <section className="py-5 bg-light">
<Container fluid className="px-5 my-5">
<Row className="gx-5 justify-content-center">
<Col className="lg-10 xl-7">
{ item
? <div className="text-center">
<div className="fs-4 mb-4 fst-italic" dangerouslySetInnerHTML={{ __html: item.text }}></div>
<div className="d-flex align-items-center justify-content-center">
<img className="rounded-circle me-3" {...item.reviewer.image} />
<div className="fw-bold">{item.reviewer.fullName}<span className="fw-bold text-primary mx-1">/</span>{item.reviewer.position}
</div>
</div>
</div>
: '' }
</Col>
</Row>
</Container>
</section>
}
interface FeaturedBlogs extends FeaturedBlogsSectionModel {
items?: BlogItemModel []
}
const FeaturedBlogsSection: FC<FeaturedBlogs> = ({
title = "",
text = "",
items = [] }) => <section className="py-5">
<Container fluid className="px-5 my-5">
<Row className="gx-5 justify-content-center">
<Col className="lg-8 xl-6">
<div className="text-center">
<h2 className="fw-bolder">{title}</h2>
<p className="lead fw-normal text-muted mb-5" dangerouslySetInnerHTML={{ __html: text }}></p>
</div>
</Col>
</Row>
<Row className="gx-5">
{items.map((item, index) => <Col key={index} className="lg-4 mb-5">
<Card className="h-100 shadow border-0">
<CardImg top {...item.image} />
<CardBody className="p-4">
<div className="badge bg-primary bg-gradient rounded-pill mb-2">{item.badges}</div>
<Link className="text-decoration-none link-dark stretched-link" to="#!">
<h5 className="card-title mb-3">{item.title}</h5>
</Link>
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: text ? text : '' }}></p>
</CardBody>
<CardFooter className="p-4 pt-0 bg-transparent border-top-0">
<div className="d-flex align-items-end justify-content-between">
<div className="d-flex align-items-center">
<img className="rounded-circle me-3" {...item.author.image} />
<div className="small">
<div className="fw-bold">{item.author.nickName}</div>
<div className="text-muted">{dateFormat(item.created)} &middot; {item.readTime}</div>
</div>
</div>
</div>
</CardFooter>
</Card>
</Col>)}
</Row>
</Container>
</section>
const CallToActionSection: FC<CallToActionSectionModel> = ({
title,
text,
privacyDisclaimer,
email = {
placeHolder: "",
title: ""
}
}) => {
return <section className="py-5">
<Container fluid className="px-5 my-5">
<aside className="bg-primary bg-gradient rounded-3 p-4 p-sm-5 mt-5">
<div className="d-flex align-items-center justify-content-between flex-column flex-xl-row text-center text-xl-start">
<div className="mb-4 mb-xl-0">
<div className="fs-3 fw-bold text-white">{title}</div>
<div className="text-white-50">{text}</div>
</div>
<div className="ms-xl-4">
<div className="input-group mb-2">
<input className="form-control" type="text" placeholder={email.placeHolder ? email.placeHolder : ''} aria-label={email.placeHolder ? email.placeHolder : ''} aria-describedby="button-newsletter" />
<button className="btn btn-outline-light" id="button-newsletter" type="button">{email.title ? email.title : ''}</button>
</div>
<div className="small text-white-50">{privacyDisclaimer}</div>
</div>
</div>
</aside>
</Container>
</section>
}
const Home = () => {
const Home : FC<IHomePageComponent> = () => {
const dispatch = useDispatch()
const { content, blogFeatured } = useSelector((state: ApplicationState) => state)
const page = content?.homePage
useEffect(() => {
dispatch(blogFeaturedActionCreators.requestBlogFeatured())
}, [])
useEffect(() => {
content?.isLoading || blogFeatured?.isLoading
? dispatch(loaderActionCreators.show())
: setTimeout(() => {
dispatch(loaderActionCreators.hide())
}, 1000)
}, [content?.isLoading, blogFeatured?.isLoading])
dispatch(headerActionCreators.updateHeader(content?.homePage.header))
}, [content?.homePage.header])
const {
header = {},
titleSection = {
title: "",
text: ""
},
featuresSection = {},
testimonialsSection = {},
featuredBlogsSection = {},
callToActionSection = {
title: "",
text: "",
privacyDisclaimer: "",
email: {
placeHolder: "",
title: ""
}
}
} = content?.homePage ? content.homePage : {}
if(content?.homePage) {
const { titleSection, featuresSection, testimonialsSection, featuredBlogsSection, callToActionSection } = content.homePage
useEffect(() => {
dispatch(headerActionCreators.updateHeader(header as HeaderModel))
}, [header])
return <>
<TitleSection {...titleSection} />
<FeaturesSection {...featuresSection} />
<TestimonialsSection {...testimonialsSection} />
<FeaturedBlogsSection items={blogFeatured?.items} {...featuredBlogsSection} />
<CallToActionSection {...callToActionSection} />
</>
return <>
<TitleSection {...titleSection} />
<FeaturesSection {...featuresSection} />
<TestimonialsSection {...testimonialsSection} />
<FeaturedBlogsSection {...{
items: blogFeatured?.items,
...featuredBlogsSection} as IFeaturedBlogsFull} />
<CallToActionSection {...callToActionSection} />
</>
}
else {
return <></>
}
}
export {

View File

@ -1,9 +1,8 @@
// React
import React, { useEffect, useState } from 'react'
import React, { FC, useEffect, useState } from 'react'
// Redux
import { useDispatch, useSelector } from 'react-redux'
import { actionCreators as loaderActionCreators } from '../../../store/reducers/Loader'
import { actionCreators as shopCartActionCreators } from '../../../store/reducers/ShopCart'
import { ApplicationState } from '../../../store'
@ -14,7 +13,32 @@ import { FeatherIcon } from '../../../components/FeatherIcons'
import style from './scss/style.module.scss'
import { ReservedWords } from '../../../enumerations'
const Cart = () => {
interface IImage {
src: string,
alt: string
}
export interface IShopCartItem {
slug: string
sku: string,
image: IImage,
title: string,
brandName: string,
shortText: string,
created: string,
price: number,
newPrice?: number,
quantity: number
}
export interface IShopCartPage {
}
export interface IShopCartComponent {}
const Cart : FC<IShopCartComponent> = () => {
const dispatch = useDispatch()
const { content, shopCart } = useSelector((state: ApplicationState) => state)
@ -23,40 +47,11 @@ const Cart = () => {
dispatch(shopCartActionCreators.requestCart({ pathParams: { userId: "fdc5aa50-ee68-4bae-a8e6-b8ae2c258f60" }}))
}, [])
useEffect(() => {
content?.isLoading
? dispatch(loaderActionCreators.show())
: setTimeout(() => {
dispatch(loaderActionCreators.hide())
}, 1000)
}, [content?.isLoading])
const {
currencySymbol = ""
} = content?.localization ? content.localization : {}
const {
titleSection = {
title: "",
text: ""
},
productsSection = {
product: "",
price: "",
quantity: "",
subtotal: "",
continueShopping: {
target: "!#",
anchorText: ""
},
checkout: {
target: "!#",
anchorText: ""
}
}
} = content?.shopCart ? content.shopCart : {}
//const { titleSection, productsSection } = content.shopCart
const [subtotal, setSubtotal] = useState<number>(0)
@ -86,7 +81,7 @@ const Cart = () => {
}
return <Container fluid>
<section className="pt-5 pb-5">
{/* <section className="pt-5 pb-5">
<div className="row w-100">
<div className="col-lg-12 col-md-12 col-12">
<h3 className="display-5 mb-2 text-center">{titleSection.title}</h3>
@ -105,7 +100,7 @@ const Cart = () => {
<td data-th="Product">
<div className="row">
<div className="col-md-3 text-left">
<img src={`${process.env.REACT_APP_API}/Image/250x250/ced4da/6c757d`} alt="" className="img-fluid d-none d-md-block rounded mb-2 shadow" />
<img src={`${process.env.REACT_APP_FRONTEND}/Image/250x250/ced4da/6c757d`} alt="" className="img-fluid d-none d-md-block rounded mb-2 shadow" />
</div>
<div className="col-md-9 text-left mt-sm-2">
<h4>{item.title}</h4>
@ -147,7 +142,7 @@ const Cart = () => {
<Link to={productsSection.continueShopping.target}><FeatherIcon icon="arrow-left" /> {productsSection.continueShopping.anchorText}</Link>
</Col>
</Row>
</section>
</section> */}
</Container>
}

View File

@ -0,0 +1,123 @@
// React
import React, { FC } from 'react'
import { Link, useNavigate } from 'react-router-dom'
// Redux
import { useDispatch } from 'react-redux'
// Reducers
import { actionCreators as shopCatalogActionCreators } from '../../../store/reducers/ShopCatalog'
// Reactstrap
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
// Components
import { FeatherRating } from '../../../components/FeatherRating'
import { SSRPagination, ISSRPaginationComponent } from '../../../components/Pagination'
interface IImage {
src: string,
alt: string
}
interface IAuthor {
id: string,
image?: IImage
nickName: string
}
export interface IShopItem {
id: string,
slug: string,
image: IImage,
badges: string [],
title: string,
shortText?: string,
text?: string,
author: IAuthor,
created: string,
tags: string []
images?: IImage [],
sku: string,
brandName: string,
rating?: number,
price: number,
newPrice?: number,
quantity?: number
}
interface IShopItems {
path: string
totalPages?: number,
currentPage?: number,
items?: IShopItem []
}
export interface IShopItemsSection {
addToCart: string
}
export interface IShopItemComponent extends IShopItemsSection, IShopItems {
currencySymbol: string,
}
const ShopItemsSection: FC<IShopItemComponent> = (props) => {
const { currencySymbol, addToCart, path = "", totalPages, currentPage, items = [] } = props
const dispatch = useDispatch()
const navigate = useNavigate()
return <>
{items.map((item, index) => <Col key={index} className="lg-6 mb-3">
<Card className="h-100">
<div className="position-absolute" style={{top: "0.5rem", right: "0.5rem"}}>
{(item?.badges ? item.badges : []).map((badge, index) => <div key={index} className="badge bg-dark text-white" style={{marginLeft: "0.5rem"}}>{badge}</div>) }
</div>
<Link to={`${path}/${currentPage}/${item.slug}`}>
<CardImg top {...item.image} />
</Link>
<CardBody>
<div className="text-center">
<h5 className="fw-bolder">{item.title}</h5>
<FeatherRating {...{
value: item?.rating ? item.rating : 0
}} />
{item.newPrice
? <><span className="text-muted text-decoration-line-through">{currencySymbol}{item.price.toFixed(2)}</span> <span>{currencySymbol}{item.newPrice.toFixed(2)}</span></>
: <span>{currencySymbol}{item.price.toFixed(2)}</span>}
</div>
</CardBody>
<CardFooter className="p-4 pt-0 border-top-0 bg-transparent">
<div className="text-center"><a className="btn btn-outline-dark mt-auto" href="#">{addToCart}</a></div>
</CardFooter>
</Card>
</Col>)}
<SSRPagination {...{
totalPages: totalPages,
currentPage: currentPage,
// onClick: (nextPage) => {
// dispatch(shopCatalogActionCreators.requestShopCatalog({
// searchParams: {
// currentPage: nextPage + ""
// }
// }))
// navigate(`${path}/${nextPage}`)
// }
linksPath: path
} as ISSRPaginationComponent} />
</>
}
export {
ShopItemsSection
}

View File

@ -0,0 +1,30 @@
// React
import React, { FC } from 'react'
// Reactstrap
import { Container } from 'reactstrap'
export interface ITitleSection {
title: string,
text: string
}
interface ITitleComponent extends ITitleSection { }
const TitleSection: FC<ITitleComponent> = (props) => {
const { title, text } = props
return <header className="py-5 bg-dark border-bottom mb-4">
<Container fluid>
<div className="text-center text-white my-5">
<h1 className="display-4 fw-bolder">{title}</h1>
<p className="lead fw-normal text-white-50 mb-0">{text}</p>
</div>
</Container>
</header>
}
export {
TitleSection
}

View File

@ -1,156 +1,103 @@
// React
import React, { FC, useEffect } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { useLocation, useParams } from 'react-router-dom'
// Redux
import { useDispatch, useSelector } from 'react-redux'
import { ApplicationState } from '../../../store'
import { actionCreators as loaderActionCreators } from '../../../store/reducers/Loader'
// Reducers
import { actionCreators as shopCatalogActionCreators } from '../../../store/reducers/ShopCatalog'
import { actionCreators as shopCategoriesActionCreators } from '../../../store/reducers/ShopCategories'
import { actionCreators as shopFeaturedActionCreators } from '../../../store/reducers/ShopFeatured'
// Reactstrap
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
import { Col, Container, Row } from 'reactstrap'
// Models (interfaces)
import { ShopItemModel } from '../../../models'
import { TitleSectionModel } from '../../../models/pageSections'
// Components
import { TitleSection, ITitleSection } from './TitleSection'
import { ShopItemsSection, IShopItemsSection, IShopItemComponent } from './ShopItemsSection'
import { Categories, Empty, Search } from '../../../components/SideWidgets'
// Custom components
import { FeatherRating } from '../../../components/FeatherRating'
import { Pagination } from '../../../components/Pagination'
// Interfaces
import { IHeader } from '../../../interfaces'
import { cloneObject } from '../../../functions'
// Functions
import { findRoutes } from '../../../functions'
const TitleSection: FC<TitleSectionModel> = ({
title = "",
text = ""
}) => <header className="bg-dark py-5">
<Container fluid className="px-4 px-lg-5 my-5">
<div className="text-center text-white">
<h1 className="display-4 fw-bolder">{title}</h1>
<p className="lead fw-normal text-white-50 mb-0">{text}</p>
</div>
</Container>
</header>
interface ShopItems {
currencySymbol: string,
addToCart: string,
path: string
totalPages?: number,
currentPage?: number,
items?: ShopItemModel []
export interface IShopCatalogPage {
header: IHeader,
titleSection: ITitleSection,
shopItemsSection: IShopItemsSection,
}
const ShopItemsSection: FC<ShopItems> = ({
currencySymbol = "",
addToCart = "",
path = "",
totalPages = 1,
currentPage = 1,
items = []
}) => {
export interface IShopCatalogComponent extends IShopCatalogPage { }
const dispatch = useDispatch()
const navigate = useNavigate()
return <section className="py-5">
<Container fluid className="px-4 px-lg-5 mt-5">
<Row className="gx-4 gx-lg-5 row-cols-2 row-cols-md-3 row-cols-xl-4 justify-content-center">
{items.map((item, index) => <Col key={index} className="mb-5">
<Card className="h-100">
<div className="position-absolute" style={{top: "0.5rem", right: "0.5rem"}}>
{(item?.badges ? item.badges : []).map((badge, index) => <div key={index} className="badge bg-dark text-white" style={{marginLeft: "0.5rem"}}>{badge}</div>) }
</div>
<Link to={`${path}/${currentPage}/${item.slug}`}>
<CardImg top {...item.image} />
</Link>
<CardBody>
<div className="text-center">
<h5 className="fw-bolder">{item.title}</h5>
<FeatherRating {...{
value: item?.rating ? item.rating : 0
}} />
{item.newPrice
? <><span className="text-muted text-decoration-line-through">{currencySymbol}{item.price.toFixed(2)}</span> <span>{currencySymbol}{item.newPrice.toFixed(2)}</span></>
: <span>{currencySymbol}{item.price.toFixed(2)}</span>}
</div>
</CardBody>
<CardFooter className="p-4 pt-0 border-top-0 bg-transparent">
<div className="text-center"><a className="btn btn-outline-dark mt-auto" href="#">{addToCart}</a></div>
</CardFooter>
</Card>
</Col>)}
</Row>
<Pagination {...{
totalPages: totalPages,
currentPage: currentPage,
onClick: (nextPage) => {
dispatch(shopCatalogActionCreators.requestShopCatalog({
searchParams: {
currentPage: nextPage + ""
}
}))
navigate(`${path}/${nextPage}`)
}
}} />
</Container>
</section>
}
const ShopCatalog = () => {
const ShopCatalog : FC<IShopCatalogComponent> = () => {
const location = useLocation()
const params = useParams()
const dispatch = useDispatch()
const { content, shopCatalog } = useSelector((state: ApplicationState) => state)
const page = content?.shopCatalog
const path = findRoutes(content?.routes, 'ShopCatalog')[0]?.targets[0]
const { content, shopCatalog, shopCategories } = useSelector((state: ApplicationState) => state)
const {
currencySymbol = ""
} = content?.localization ? content.localization : {}
const { dateFormat, timeFormat, currencySymbol } = content.localization
const { header, titleSection, shopItemsSection } = content.shopCatalog
useEffect(() => {
dispatch(shopCategoriesActionCreators.requestShopCategories())
dispatch(shopFeaturedActionCreators.requestShopFeatured())
}, [])
useEffect(() => {
dispatch(shopCatalogActionCreators.requestShopCatalog({
searchParams: {
currentPage: params?.page ? params.page : "1"
category: params?.category,
currentPage: params?.page
}
}))
}, [])
}, [params.category, params.page])
useEffect(() => {
shopCatalog?.isLoading
? dispatch(loaderActionCreators.show())
: setTimeout(() => {
dispatch(loaderActionCreators.hide())
}, 1000)
}, [shopCatalog?.isLoading])
const updateShopCategories = () => {
const newShopCategoies = cloneObject(shopCategories)
const {
shopItemsSection = {
addToCart: ""
}
} = content?.shopCatalog ? content?.shopCatalog : {}
newShopCategoies.items = newShopCategoies.items.map(item => {
item.href = `${location.pathname.split('/').slice(0, 2).join('/')}/${item.href}`
return item
})
const shopItems: ShopItems = {
currencySymbol,
path,
...shopItemsSection,
...shopCatalog
return newShopCategoies
}
const updateShopLinks = () => {
return location.pathname.split('/').slice(0, 3).join('/')
}
return <>
<TitleSection {...page?.titleSection} />
<ShopItemsSection {...shopItems} />
<TitleSection {...titleSection} />
<Container fluid>
<Row>
<Col>
<Row>
<ShopItemsSection {...{
currencySymbol,
path: updateShopLinks(),
...shopItemsSection,
...shopCatalog
} as IShopItemComponent} />
</Row>
</Col>
<Col lg="3">
<Search />
<Categories {...updateShopCategories()} />
<Empty/>
</Col>
</Row>
</Container>
</>
}

View File

@ -1,10 +1,16 @@
import React from 'react'
import React, { FC } from 'react'
import { Container } from 'reactstrap'
// CSS Modules
import style from './scss/style.module.scss'
const Checkout = () => {
export interface IShopCheckoutPage {
}
export interface IShopCheckoutComponent extends IShopCheckoutPage {}
const Checkout : FC<IShopCheckoutComponent> = () => {
return <Container fluid className={`py-5 ${style.container}`}>
<main>
<div className="text-center">

View File

@ -0,0 +1,77 @@
import React, { FC } from 'react'
import { Card, CardBody } from 'reactstrap'
interface IImage {
src: string,
alt: string
}
interface IAuthor {
id: string,
image?: IImage
nickName: string
}
export interface IComment {
author: IAuthor,
comment: string,
responses?: IComment []
}
export interface IComments {
path?: string
totalPages?: number,
currentPage?: number,
items?: IComment []
}
export interface ICommentsSection {
leaveComment: string
}
export interface ICommentsComponent extends ICommentsSection, IComments { }
const CommentsComponent: FC<ICommentsComponent> = (props) => {
const { leaveComment, items = [] } = props
return <section className="mb-5">
<Card className="card bg-light">
<CardBody className="card-body">
<form className="mb-4">
<textarea className="form-control" rows={3} placeholder={leaveComment}></textarea>
</form>
{items.map((comment, index) => <div key={index} className={`d-flex ${index < items.length - 1 ? 'mb-4' : ''}`}>
<div className="flex-shrink-0">
<img className="rounded-circle" {...comment.author.image} />
</div>
<div className="ms-3">
<div className="fw-bold">{comment.author.nickName}</div>
{comment.comment}
{comment.responses? comment.responses.map((response, index) => <div key={index} className="d-flex mt-4">
<div className="flex-shrink-0">
<img className="rounded-circle" {...response.author.image} />
</div>
<div className="ms-3">
<div className="fw-bold">{response.author.nickName}</div>
{response.comment}
</div>
</div>) : ''}
</div>
</div>)}
</CardBody>
</Card>
</section>
}
export {
CommentsComponent
}

View File

@ -4,7 +4,6 @@ import React, { FC, useEffect } from "react"
// Reduc
import { useDispatch, useSelector } from "react-redux"
import { ApplicationState } from "../../../store"
import { actionCreators as loaderActionCreators } from '../../../store/reducers/Loader'
// Reactstrap
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from "reactstrap"
@ -12,37 +11,73 @@ import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from "reacts
// Components
import { FeatherRating } from "../../../components/FeatherRating"
const RelatedProducts: FC = () => {
interface IImage {
src: string,
alt: string
}
interface IAuthor {
id: string,
image?: IImage
nickName: string
}
export interface IRelatedProduct {
id: string,
slug: string,
image: IImage,
badges: string [],
title: string,
shortText?: string,
text?: string,
author: IAuthor,
created: string,
tags: string []
images?: IImage [],
sku: string,
brandName: string,
rating?: number,
price: number,
newPrice?: number,
quantity?: number
}
export interface IRelatedProducts {
items?: IRelatedProduct []
}
export interface IRelatedProductsSection {
title: string,
addToCart: string
}
export interface IRelatedProductsComponent extends IRelatedProductsSection, IRelatedProducts {
}
const RelatedProducts: FC<IRelatedProductsComponent> = (props) => {
const dispatch = useDispatch()
const { content, shopRelated } = useSelector((state: ApplicationState) => state)
const {
currencySymbol = ""
} = content?.localization ? content.localization : {}
const { currencySymbol } = content.localization
const {
relatedProductsSection = {
title: "",
addToCart: ""
}
} = content?.shopItem ? content?.shopItem : {}
const { relatedProductsSection } = content.shopItem
const {
items = []
} = shopRelated ? shopRelated : {}
useEffect(() => {
content?.isLoading || shopRelated?.isLoading
? dispatch(loaderActionCreators.show())
: setTimeout(() => {
dispatch(loaderActionCreators.hide())
}, 1000)
}, [content?.isLoading, shopRelated?.isLoading])
return <section className="py-5 bg-light">
<Container fluid className="px-4 px-lg-5">
<h2 className="fw-bolder mb-4">{relatedProductsSection.title}</h2>
{/* <h2 className="fw-bolder mb-4">{relatedProductsSection.title}</h2>
<Row className="gx-4 gx-lg-5 row-cols-2 row-cols-md-3 row-cols-xl-4 justify-content-center">
{items.map((item, index) => <Col key={index} className="mb-5">
@ -69,7 +104,7 @@ const RelatedProducts: FC = () => {
</CardFooter>
</Card>
</Col>)}
</Row>
</Row> */}
</Container>
</section>
}

View File

@ -4,8 +4,8 @@ import { 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 shopItemActionCreators } from '../../../store/reducers/ShopItem'
// Reactstrap
@ -13,13 +13,71 @@ import { Container } from 'reactstrap'
// Components
import { FeatherIcon } from '../../../components/FeatherIcons'
import { RelatedProducts } from '../RelatedProducts'
import { IRelatedProductsComponent, IRelatedProductsSection, RelatedProducts } from './RelatedProducts'
const ShopItem : FC = () => {
import { IHeader } from '../../../interfaces'
import { CommentsComponent, ICommentsComponent, ICommentsSection } from './CommentsComponent'
interface IImage {
src: string,
alt: string
}
interface IAuthor {
id: string,
image?: IImage
nickName: string
}
export interface IShopItem {
id: string,
slug: string,
image: IImage,
badges: string [],
title: string,
shortText?: string,
text?: string,
author: IAuthor,
created: string,
tags: string []
images?: IImage [],
sku: string,
brandName: string,
rating?: number,
price: number,
newPrice?: number,
quantity?: number
}
interface IProductSection {
title?: string
text?: string
availableQuantity: string,
addToCart: string
}
export interface IShopItemPage {
header: IHeader,
productSection: IProductSection
relatedProductsSection: IRelatedProductsSection,
commentsSection: ICommentsSection
}
export interface IShopItemComponent extends IShopItemPage {}
const ShopItem : FC<IShopItemComponent> = () => {
const params = useParams()
const dispatch = useDispatch()
const { content, shopItem } = useSelector((state: ApplicationState) => state)
const { content, shopItem, comments } = useSelector((state: ApplicationState) => state)
const { commentsSection } = content.shopItem
const {
currencySymbol = ""
@ -41,17 +99,6 @@ const ShopItem : FC = () => {
}))
}, [])
useEffect(() => {
content?.isLoading || shopItem?.isLoading
? dispatch(loaderActionCreators.show())
: setTimeout(() => {
dispatch(loaderActionCreators.hide())
}, 1000)
}, [content?.isLoading, shopItem?.isLoading])
return <>
<section className="py-5">
<Container fluid className="px-4 px-lg-5 my-5">
@ -81,7 +128,15 @@ const ShopItem : FC = () => {
</Container>
</section>
<RelatedProducts />
<RelatedProducts {...{
} as IRelatedProductsComponent} />
<CommentsComponent {...{
...commentsSection,
...comments
} as ICommentsComponent} />
</>
}

View File

@ -1,14 +1,15 @@
import React, { useEffect, useState } from "react"
import React, { FC, useEffect, useState } from "react"
// Redux
import { useDispatch, useSelector } from "react-redux"
import { actionCreators as loaderActionCreators } from '../../store/reducers/Loader'
import { Link } from "react-router-dom"
import { Button, Container, Form, FormGroup, Input, Label } from "reactstrap"
import { ApplicationState } from "../../store"
import './scss/style.scss'
import { IHeader } from "../../interfaces"
import { Post } from "../../restClient"
interface IStateProp {
[key: string]: string;
@ -19,38 +20,36 @@ interface IState extends IStateProp {
password: string
}
const Signin = () => {
interface IFormControl {
title: string,
placeHolder?: string
}
interface ILink {
target: string,
anchorText: string
}
export interface ISigninPage {
header: IHeader,
title: string,
email: IFormControl,
password: IFormControl,
dontHaveAnAccount: string,
signUpLink: ILink,
submit: IFormControl
}
export interface ISigninComponent extends ISigninPage {
}
const Signin : FC<ISigninComponent> = () => {
const dispatch = useDispatch()
const { content } = useSelector((state: ApplicationState) => state)
const {
title = "",
email = {
title: "",
placeHolder: ""
},
password = {
title: "",
placeHolder: ""
},
dontHaveAnAccount = "",
signUpLink = {
target: "#",
anchorText: ""
},
submit = {
title: ""
}
} = content?.signIn ? content.signIn : {}
useEffect(() => {
content?.isLoading
? dispatch(loaderActionCreators.show())
: setTimeout(() => {
dispatch(loaderActionCreators.hide())
}, 1000)
}, [content?.isLoading])
const { title, email, password, dontHaveAnAccount, signUpLink, submit } = content.signIn
const [state, hookState] = useState<IState>({
username: '',
@ -69,6 +68,18 @@ const Signin = () => {
setState({ [name]: value })
}
const postSignIn = () => {
Post<Promise<any>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_ACCOUNT}`, { account: 'Authenticate' }, {
username: state.username,
password: state.password
}).then(response => response)
.then((data) => {
if(data) {
console.log(data)
}
})
}
return <Container className="container">
<h2>{title}</h2>
<Form className="form">
@ -95,7 +106,7 @@ const Signin = () => {
<FormGroup>
{dontHaveAnAccount} <Link to={signUpLink.target}>{signUpLink.anchorText}</Link>.
</FormGroup>
<Button>{submit.title}</Button>
<Button onClick={postSignIn}>{submit.title}</Button>
</Form>

View File

@ -1,13 +1,13 @@
import React, { useEffect, useState } from "react"
import React, { FC, useEffect, useState } from "react"
// Redux
import { useDispatch, useSelector } from "react-redux"
import { actionCreators as loaderActionCreators } from '../../store/reducers/Loader'
import { Button, Container, Form, FormGroup, Input, Label } from "reactstrap"
import { ApplicationState } from "../../store"
import './scss/style.scss'
import { IHeader } from "../../interfaces"
interface IStateProp {
[key: string]: string | boolean;
@ -25,46 +25,39 @@ interface IState extends IStateProp {
tnc: boolean
}
const Signup = () => {
interface IFormControl {
title: string,
placeHolder?: string
}
export interface ISignupPage {
header: IHeader,
title: string,
username: IFormControl,
email: IFormControl,
reEmail: IFormControl,
password: IFormControl,
rePassword: IFormControl,
acceptTermsAndConditions: string,
submit: IFormControl
}
export interface ISignupComponent extends ISignupPage { }
const Signup : FC<ISignupComponent> = () => {
const dispatch = useDispatch()
const { content } = useSelector((state: ApplicationState) => state)
const {
title = "",
username = {
title: "",
placeHolder: ""
},
email = {
title: "",
placeHolder: ""
},
reEmail = {
title: "Repeat email address",
placeHolder: "Repeat email address..."
},
password = {
title: "",
placeHolder: ""
},
rePassword = {
title: "Repeat password",
placeHolder: "Repeat password..."
},
title, username,
email,
reEmail,
password,
rePassword,
acceptTermsAndConditions = "",
submit = {
title: ""
}
} = content?.signUp ? content.signUp : {}
useEffect(() => {
content?.isLoading
? dispatch(loaderActionCreators.show())
: setTimeout(() => {
dispatch(loaderActionCreators.hide())
}, 1000)
}, [content?.isLoading])
submit
} = content.signUp
const [state, hookState] = useState<IState>({
username: '',
@ -96,7 +89,7 @@ const Signup = () => {
}
return <Container className="container">
<h2>{title}</h2>
{/* <h2>{title}</h2>
<Form className="form">
<FormGroup>
<Label for="username">{username.title}</Label>
@ -153,7 +146,7 @@ const Signup = () => {
<Label check>{acceptTermsAndConditions}</Label>
</FormGroup>
<Button>{submit.title}</Button>
</Form>
</Form> */}
</Container>
}

View File

@ -1,18 +1,47 @@
import { Params, RequestModel } from "./models/abstractions"
import axios from "axios"
import { IParams } from "./interfaces"
interface FetchData {
export interface FetchResult<T> {
status: number,
text: string
data?: T
}
const Post = () => {
const Post = async <T>(apiUrl: string, pathParams?: IParams, data?: any) : Promise<FetchResult<T>> => {
const url = new URL(apiUrl)
if(pathParams) {
Object.keys(pathParams).forEach(key => {
if (typeof(pathParams[key]) !== undefined) {
url.pathname += `/${pathParams[key]}`
}
})
}
const requestParams = {
method: 'POST',
headers: { 'accept': 'application/json', 'content-type': 'application/json' },
data
}
const fetchData = await axios(url.toString(), requestParams)
.then(async fetchData => {
return {
status: fetchData.status,
data: fetchData.data as T
}
})
.catch(err => {
console.log(err)
return {
status: err.status
}
})
return fetchData
}
const Get = async <T>(apiUrl: string, pathParams?: Params, searchParams?: Params): Promise<T | null> => {
const Get = async <T>(apiUrl: string, pathParams?: IParams, searchParams?: IParams): Promise<T | null> => {
const url = new URL(apiUrl)
if(pathParams) {
@ -36,21 +65,19 @@ const Get = async <T>(apiUrl: string, pathParams?: Params, searchParams?: Params
headers: { 'accept': 'application/json', 'content-type': 'application/json' },
}
const fetchData = await fetch(url.toString(), requestParams)
const fetchData = await axios(url.toString(), requestParams)
.then(async fetchData => {
return {
status: fetchData.status,
text: await fetchData.text()
}
// console.log(fetchData)
return fetchData.data as T
})
.catch(err => {
console.log(err)
})
if (fetchData?.text)
return JSON.parse((fetchData as FetchData).text) as T
return null
return null
})
return fetchData
}
const Put = () => {
@ -67,6 +94,3 @@ export {
Put,
Delete
}

View File

@ -5,8 +5,8 @@ import * as BlogItem from './reducers/BlogItem'
import * as Counter from './reducers/Counter'
import * as Header from './reducers/Header'
import * as Loader from './reducers/Loader'
import * as Comments from './reducers/Comments'
import * as Content from './reducers/Content'
import * as ShopCatalog from './reducers/ShopCatalog'
@ -20,25 +20,25 @@ import * as WeatherForecasts from './reducers/WeatherForecasts'
// The top-level state object
export interface ApplicationState {
blogCatalog: BlogCatalog.BlogCatalogState | undefined
blogCategories: BlogCategories.BlogCategoriesState | undefined
blogFeatured: BlogFeatured.BlogFeaturedState | undefined
blogItem: BlogItem.BlogItemState | undefined
blogCatalog: BlogCatalog.BlogCatalogState
blogCategories: BlogCategories.BlogCategoriesState
blogFeatured: BlogFeatured.BlogFeaturedState
blogItem: BlogItem.BlogItemState
content: Content.ContentState | undefined
comments: Comments.CommentsState
content: Content.ContentState
counter: Counter.CounterState | undefined
header: Header.HeaderState | undefined
loader: Loader.LoaderState | undefined
counter: Counter.CounterState
header: Header.HeaderState
shopCatalog: ShopCatalog.ShopCatalogState | undefined
shopCategories: ShopCategories.ShopCategoriesState | undefined
shopFeatured: ShopFeatured.ShopFeaturedState | undefined
shopItem: ShopItem.ShopItemState | undefined
shopRelated: ShopRelated.ShopRelatedState | undefined
shopCart: ShopCart.ShopCartState | undefined
shopCatalog: ShopCatalog.ShopCatalogState
shopCategories: ShopCategories.ShopCategoriesState
shopFeatured: ShopFeatured.ShopFeaturedState
shopItem: ShopItem.ShopItemState
shopRelated: ShopRelated.ShopRelatedState
shopCart: ShopCart.ShopCartState
weatherForecasts: WeatherForecasts.WeatherForecastsState | undefined
weatherForecasts: WeatherForecasts.WeatherForecastsState
}
// Whenever an action is dispatched, Redux will update each top-level application state property using
@ -50,11 +50,11 @@ export const reducers = {
blogFeatured: BlogFeatured.reducer,
blogItem: BlogItem.reducer,
comments: Comments.reducer,
content: Content.reducer,
counter: Counter.reducer,
header: Header.reducer,
loader: Loader.reducer,
shopCatalog: ShopCatalog.reducer,
shopCategories: ShopCategories.reducer,

View File

@ -1,44 +1,46 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { GetBlogCatalogRequestModel } from '../../models/requests'
import { GetBlogCatalogResponseModel } from '../../models/responses'
import { Get } from '../../restClient'
// Interfaces
import { IPagination, IParams, IRequest, IResponse } from '../../interfaces'
import { IBlogItem } from '../../pages/Blog/Catalog/BlogItemsComponent'
export interface BlogCatalogState extends GetBlogCatalogResponseModel {
import { Get } from '../../restClient'
import { cloneObject } from '../../functions'
// Request
interface IGetBlogcatalogPathParams extends IParams {}
interface IGeBlogCatalogSearchParams extends IParams {
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
interface IGetBlogCatalogRequestModel extends IRequest<IGetBlogcatalogPathParams, IGeBlogCatalogSearchParams> { }
// Response
interface IGetBlogCatalogResponseModel extends IPagination<IBlogItem>, IResponse { }
export interface BlogCatalogState extends IGetBlogCatalogResponseModel {
isLoading: boolean
}
interface RequestAction extends GetBlogCatalogRequestModel {
interface RequestAction extends IGetBlogCatalogRequestModel {
type: 'REQUEST_BLOG_CATALOG'
}
interface ReceiveAction extends GetBlogCatalogResponseModel {
interface ReceiveAction extends IGetBlogCatalogResponseModel {
type: 'RECEIVE_BLOG_CATALOG'
}
type KnownAction = RequestAction | ReceiveAction
export const actionCreators = {
requestBlogCatalog: (props?: GetBlogCatalogRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
const locale = getState().content?.localization.locale
const searchParams = {...props?.searchParams, locale}
Get<Promise<GetBlogCatalogResponseModel>>(`${process.env.REACT_APP_API}/Image/${process.env.REACT_APP_BLOGITEMS}/${process.env.REACT_APP_SITEID}`, props?.pathParams, searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_BLOG_CATALOG', ...data })
})
dispatch({ type: 'REQUEST_BLOG_CATALOG' })
}
}
const unloadedState: BlogCatalogState = {
totalPages: 1,
const mockData: IGetBlogCatalogResponseModel = {
totalPages: 100,
currentPage: 1,
items: [
{
@ -46,17 +48,16 @@ const unloadedState: BlogCatalogState = {
slug: "demo-post",
badges: [ "demo" ],
image: {
src: `${process.env.REACT_APP_API}/Image/850x350/dee2e6/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/850x350/dee2e6/6c757d`,
alt: "..."
},
title: "Lorem ipsum",
shortText: "",
text: "",
shortText: "This is a blog short text...",
author: {
id: "",
nickName: "Admin",
image: {
src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`,
alt: "..."
}
},
@ -70,17 +71,16 @@ const unloadedState: BlogCatalogState = {
slug: "demo-post",
badges: [ "demo" ],
image: {
src: `${process.env.REACT_APP_API}/Image/850x350/dee2e6/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/850x350/dee2e6/6c757d`,
alt: "..."
},
title: "Lorem ipsum",
shortText: "",
text: "",
shortText: "This is a blog short text...",
author: {
id: "",
nickName: "Admin",
image: {
src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`,
alt: "..."
}
},
@ -94,17 +94,16 @@ const unloadedState: BlogCatalogState = {
slug: "demo-post",
badges: [ "demo" ],
image: {
src: `${process.env.REACT_APP_API}/Image/850x350/dee2e6/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/850x350/dee2e6/6c757d`,
alt: "..."
},
title: "Lorem ipsum",
shortText: "",
text: "",
shortText: "This is a blog short text...",
author: {
id: "",
nickName: "Admin",
image: {
src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`,
alt: "..."
}
},
@ -118,17 +117,16 @@ const unloadedState: BlogCatalogState = {
slug: "demo-post",
badges: [ "demo" ],
image: {
src: `${process.env.REACT_APP_API}/Image/850x350/dee2e6/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/850x350/dee2e6/6c757d`,
alt: "..."
},
title: "Lorem ipsum",
shortText: "",
text: "",
shortText: "This is a blog short text...",
author: {
id: "",
nickName: "Admin",
image: {
src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`,
alt: "..."
}
},
@ -137,7 +135,32 @@ const unloadedState: BlogCatalogState = {
likes: 0
}
],
]
}
export const actionCreators = {
requestBlogCatalog: (props?: IGetBlogCatalogRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
dispatch({ type: 'REQUEST_BLOG_CATALOG' })
const locale = process.env.REACT_APP_LOCALE
const searchParams = {...props?.searchParams, locale}
if(process.env.REACT_APP_LOCAL_ONLY == 'Y') {
dispatch({ type: 'RECEIVE_BLOG_CATALOG', ...cloneObject(mockData) })
return
}
Get<Promise<IGetBlogCatalogResponseModel>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_BLOGITEMS}/${process.env.REACT_APP_SITEID}`, props?.pathParams, searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_BLOG_CATALOG', ...data })
})
}
}
const unloadedState: BlogCatalogState = {
...cloneObject(mockData),
isLoading: false
}

View File

@ -1,47 +1,73 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { GetBlogCategoriesRequestModel } from '../../models/requests'
import { GetBlogCategoriesResponseModel } from '../../models/responses'
import { Get } from '../../restClient'
// Interfaces
import { IPagination, IParams, IRequest, IResponse } from '../../interfaces'
import { ICategory } from '../../components/SideWidgets/Categories'
export interface BlogCategoriesState extends GetBlogCategoriesResponseModel {
import { Get } from '../../restClient'
import { cloneObject } from '../../functions'
// Request
interface IGetBlogCategoriesPathParams extends IParams {}
interface IGetBlogCategoriesSearchParams extends IParams {}
interface IGetBlogCategoriesRequestModel extends IRequest<IGetBlogCategoriesPathParams, IGetBlogCategoriesSearchParams> { }
// Response
interface IGetBlogCategoriesResponseModel extends IPagination<ICategory>, IResponse { }
export interface BlogCategoriesState extends IGetBlogCategoriesResponseModel {
isLoading: boolean
}
interface RequestAction extends GetBlogCategoriesRequestModel {
interface RequestAction extends IGetBlogCategoriesRequestModel {
type: 'REQUEST_BLOG_CATEGORIES'
}
interface ReceiveAction extends GetBlogCategoriesResponseModel {
interface ReceiveAction extends IGetBlogCategoriesResponseModel {
type: 'RECEIVE_BLOG_CATEGORIES'
}
type KnownAction = RequestAction | ReceiveAction
const mockData: IGetBlogCategoriesResponseModel = {
totalPages: 1,
currentPage: 1,
items: [
{ href: 'default', anchorText: "Default" },
{ href: 'software', anchorText: "Software" },
{ href: 'hardware', anchorText: "Hardware" }
]
}
export const actionCreators = {
requestBlogCategories: (props?: GetBlogCategoriesRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
const locale = getState().content?.localization.locale
requestBlogCategories: (props?: IGetBlogCategoriesRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
dispatch({ type: 'REQUEST_BLOG_CATEGORIES' })
const locale = process.env.REACT_APP_LOCALE
const searchParams = {...props?.searchParams, locale}
Get<Promise<GetBlogCategoriesResponseModel>>(`${process.env.REACT_APP_API}/Image/${process.env.REACT_APP_CATEGORYITEMS}/${process.env.REACT_APP_SITEID}`, props?.pathParams, searchParams)
if(process.env.REACT_APP_LOCAL_ONLY == 'Y') {
console.log(mockData)
dispatch({ type: 'RECEIVE_BLOG_CATEGORIES', ...cloneObject(mockData) })
return
}
Get<Promise<IGetBlogCategoriesResponseModel>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_CATEGORYITEMS}/${process.env.REACT_APP_SITEID}`, props?.pathParams, searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_BLOG_CATEGORIES', ...data })
})
dispatch({ type: 'REQUEST_BLOG_CATEGORIES' })
}
}
const unloadedState: BlogCategoriesState = {
items: [
{ id: "", text: "Software" },
{ id: "", text: "Hardware" }
],
...cloneObject(mockData),
isLoading: false
}

View File

@ -1,59 +1,59 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { GetBlogFeaturedRequestModel } from '../../models/requests'
import { GetBlogFeaturedResponseModel } from '../../models/responses'
import { Get } from '../../restClient'
// Interfaces
import { IParams, IRequest, IResponse } from '../../interfaces'
import { IFeaturedBlogItem } from '../../pages/Blog/Catalog/FeaturedBlogComponent'
export interface BlogFeaturedState extends GetBlogFeaturedResponseModel {
import { Get } from '../../restClient'
import { cloneObject } from '../../functions'
// Request
interface IGetBlogFeaturedPathParams extends IParams {}
interface IGetBlogFeaturedSearchParams extends IParams {}
interface IGetBlogFeaturedRequestModel extends IRequest<IGetBlogFeaturedPathParams, IGetBlogFeaturedSearchParams> { }
// Response
interface IGetBlogFeaturedResponseModel extends IResponse {
items: IFeaturedBlogItem []
}
export interface BlogFeaturedState extends IGetBlogFeaturedResponseModel {
isLoading: boolean
}
interface RequestAction extends GetBlogFeaturedRequestModel {
interface RequestAction extends IGetBlogFeaturedRequestModel {
type: 'REQUEST_BLOG_FEATURED'
}
interface ReceiveAction extends GetBlogFeaturedResponseModel {
interface ReceiveAction extends IGetBlogFeaturedResponseModel {
type: 'RECEIVE_BLOG_FEATURED'
}
type KnownAction = RequestAction | ReceiveAction
export const actionCreators = {
requestBlogFeatured: (props?: GetBlogFeaturedRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
const locale = getState().content?.localization.locale
const searchParams = {...props?.searchParams, locale}
Get<Promise<GetBlogFeaturedResponseModel>>(`${process.env.REACT_APP_API}/Image/${process.env.REACT_APP_BLOGITEMS_FEAUTERED}/${process.env.REACT_APP_SITEID}`, props?.pathParams, searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_BLOG_FEATURED', ...data })
})
dispatch({ type: 'REQUEST_BLOG_FEATURED' })
}
}
const unloadedState: BlogFeaturedState = {
const mockData: IGetBlogFeaturedResponseModel = {
items: [
{
id: "",
slug: "demo-post",
badges: [ "demo" ],
image: {
src: `${process.env.REACT_APP_API}/Image/850x350/dee2e6/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/850x350/dee2e6/6c757d`,
alt: "..."
},
title: "Lorem ipsum",
shortText: "",
shortText: "This is a blog short text...",
author: {
id: "",
nickName: "Admin",
image: {
src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`,
alt: "..."
}
},
@ -68,16 +68,16 @@ const unloadedState: BlogFeaturedState = {
slug: "demo-post",
badges: [ "demo" ],
image: {
src: `${process.env.REACT_APP_API}/Image/850x350/dee2e6/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/850x350/dee2e6/6c757d`,
alt: "..."
},
title: "Lorem ipsum",
shortText: "",
shortText: "This is a blog short text...",
author: {
id: "",
nickName: "Admin",
image: {
src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`,
alt: "..."
}
},
@ -92,16 +92,16 @@ const unloadedState: BlogFeaturedState = {
slug: "demo-post",
badges: [ "demo" ],
image: {
src: `${process.env.REACT_APP_API}/Image/850x350/dee2e6/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/850x350/dee2e6/6c757d`,
alt: "..."
},
title: "Lorem ipsum",
shortText: "",
shortText: "This is a blog short text...",
author: {
id: "",
nickName: "Admin",
image: {
src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`,
alt: "..."
}
},
@ -111,7 +111,32 @@ const unloadedState: BlogFeaturedState = {
readTime: 10,
likes: 0
}
],
]
}
export const actionCreators = {
requestBlogFeatured: (props?: IGetBlogFeaturedRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
dispatch({ type: 'REQUEST_BLOG_FEATURED' })
const locale = process.env.REACT_APP_LOCALE
const searchParams = {...props?.searchParams, locale}
if(process.env.REACT_APP_LOCAL_ONLY == 'Y') {
dispatch({ type: 'RECEIVE_BLOG_FEATURED', ...cloneObject(mockData) })
return
}
Get<Promise<IGetBlogFeaturedResponseModel>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_BLOGITEMS_FEAUTERED}/${process.env.REACT_APP_SITEID}`, props?.pathParams, searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_BLOG_FEATURED', ...data })
})
}
}
const unloadedState: BlogFeaturedState = {
...cloneObject(mockData),
isLoading: false
}

View File

@ -1,47 +1,46 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { GetBlogItemRequestModel } from '../../models/requests'
import { GetBlogItemResponseModel } from '../../models/responses'
import { Get } from '../../restClient'
// interfaces
import { IParams, IRequest, IResponse } from '../../interfaces'
import { IBlogItem } from '../../pages/Blog/Item'
export interface BlogItemState extends GetBlogItemResponseModel {
import { Get } from '../../restClient'
import { cloneObject } from '../../functions'
// Request
interface IGetBlogItemPathParams extends IParams { }
interface IGetBlogItemSearchParams extends IParams {
slug: string
}
interface IGetBlogItemRequestModel extends IRequest<IGetBlogItemPathParams, IGetBlogItemSearchParams> { }
// Response
interface IGetBlogItemResponseModel extends IBlogItem, IResponse { }
export interface BlogItemState extends IGetBlogItemResponseModel {
isLoading: boolean
}
interface RequestAction extends GetBlogItemRequestModel {
interface RequestAction extends IGetBlogItemRequestModel {
type: 'REQUEST_BLOG_ITEM'
}
interface ReceiveAction extends GetBlogItemResponseModel {
interface ReceiveAction extends IGetBlogItemResponseModel {
type: 'RECEIVE_BLOG_ITEM'
}
type KnownAction = RequestAction | ReceiveAction
export const actionCreators = {
requestBlogItem: (props?: GetBlogItemRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
const locale = getState().content?.localization.locale
const searchParams = {...props?.searchParams, locale}
Get<Promise<GetBlogItemResponseModel>>(`${process.env.REACT_APP_API}/Image/${process.env.REACT_APP_BLOGITEM}/${process.env.REACT_APP_SITEID}`, props?.pathParams, searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_BLOG_ITEM', ...data })
})
// dispatch({ type: 'REQUEST_BLOG_ITEM', slug: props.slug })
}
}
const unloadedState: BlogItemState = {
const mockData: IGetBlogItemResponseModel = {
id: "",
slug: "demo-post",
image: {
src: `${process.env.REACT_APP_API}/Image/900x400/ced4da/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/900x400/ced4da/6c757d`,
alt: "..."
},
badges: [
@ -60,63 +59,37 @@ const unloadedState: BlogItemState = {
id: "",
nickName: "Admin",
image: {
src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`,
alt: "..."
}
},
created: new Date().toString(),
tags: [ "react", "redux", "webapi" ],
comments: [
{
author: {
id: "",
nickName: "Commenter Name 1",
image: {
src: `${process.env.REACT_APP_API}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "If you're going to lead a space frontier, it has to be government; it'll never be private enterprise. Because the space frontier is dangerous, and it's expensive, and it has unquantified risks.",
responses: [
{
author: {
id: "",
nickName: "Commenter Name 4",
image: {
src: `${process.env.REACT_APP_API}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "And under those conditions, you cannot establish a capital-market evaluation of that enterprise. You can't get investors."
},
{
author: {
id: "",
nickName: "Commenter Name 3",
image: {
src: `${process.env.REACT_APP_API}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "When you put money directly to a problem, it makes a good headline."
}
]
},
{
author: {
id: "",
nickName: "Commenter Name 2",
image: {
src: `${process.env.REACT_APP_API}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "When I look at the universe and all the ways the universe wants to kill us, I find it hard to reconcile that with statements of beneficence."
}
],
tags: [ "react", "redux", "webapi" ]
}
export const actionCreators = {
requestBlogItem: (props?: IGetBlogItemRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
dispatch({ type: 'REQUEST_BLOG_ITEM' })
const locale = process.env.REACT_APP_LOCALE
const searchParams = {...props?.searchParams, locale}
if(process.env.REACT_APP_LOCAL_ONLY == 'Y') {
dispatch({ type: 'RECEIVE_BLOG_ITEM', ...cloneObject(mockData) })
return
}
Get<Promise<IGetBlogItemResponseModel>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_BLOGITEM}/${process.env.REACT_APP_SITEID}`, props?.pathParams, searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_BLOG_ITEM', ...data })
})
}
}
const unloadedState: BlogItemState = {
...cloneObject(mockData),
isLoading: false
}

View File

@ -0,0 +1,137 @@
//Redux
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
// Interfaces
import { IPagination, IParams, IRequest, IResponse } from '../../interfaces'
import { IComment } from '../../pages/Blog/Item/CommentsComponent'
import { Get } from '../../restClient'
import { cloneObject } from '../../functions'
// Request
interface IGetCommentsPathParams extends IParams { }
interface IGetCommentsSearchParams extends IParams { }
interface IGetCommentsRequestModel extends IRequest<IGetCommentsPathParams, IGetCommentsSearchParams> { }
// Response
interface IGetCommentsResponseModel extends IPagination<IComment>, IResponse { }
export interface CommentsState extends IGetCommentsResponseModel {
isLoading: boolean
}
interface RequestAction extends IGetCommentsRequestModel {
type: 'REQUEST_COMMENTS'
}
interface ReceiveAction extends IGetCommentsResponseModel {
type: 'RECEIVE_COMMENTS'
}
type KnownAction = RequestAction | ReceiveAction
const mockData: IGetCommentsResponseModel = {
totalPages: 100,
currentPage: 1,
items: [
{
author: {
id: "",
nickName: "Commenter Name 1",
image: {
src: `${process.env.REACT_APP_FRONTEND}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "If you're going to lead a space frontier, it has to be government; it'll never be private enterprise. Because the space frontier is dangerous, and it's expensive, and it has unquantified risks.",
responses: [
{
author: {
id: "",
nickName: "Commenter Name 4",
image: {
src: `${process.env.REACT_APP_FRONTEND}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "And under those conditions, you cannot establish a capital-market evaluation of that enterprise. You can't get investors."
},
{
author: {
id: "",
nickName: "Commenter Name 3",
image: {
src: `${process.env.REACT_APP_FRONTEND}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "When you put money directly to a problem, it makes a good headline."
}
]
},
{
author: {
id: "",
nickName: "Commenter Name 2",
image: {
src: `${process.env.REACT_APP_FRONTEND}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "When I look at the universe and all the ways the universe wants to kill us, I find it hard to reconcile that with statements of beneficence."
}
]
}
export const actionCreators = {
requestComments: (props?: IGetCommentsRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
if(process.env.REACT_APP_LOCAL_ONLY == 'Y') {
dispatch({ type: 'RECEIVE_COMMENTS', ...cloneObject(mockData) })
return
}
/*
Get<Promise<IGetBlogCatalogResponseModel>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_BLOGITEMS}/${process.env.REACT_APP_SITEID}`, props?.pathParams, searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_BLOG_CATALOG', ...data })
})
dispatch({ type: 'REQUEST_BLOG_CATALOG' })
*/
}
}
const unloadedState: CommentsState = {
...cloneObject(mockData),
isLoading: false
}
export const reducer: Reducer<CommentsState> = (state: CommentsState | undefined, incomingAction: Action): CommentsState => {
if (state === undefined) {
return unloadedState
}
const action = incomingAction as KnownAction
switch (action.type) {
case 'REQUEST_COMMENTS':
return {
...state,
isLoading: true
}
case 'RECEIVE_COMMENTS':
return {
...action,
isLoading: false
}
}
return state
}

View File

@ -2,42 +2,100 @@ import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { ReservedWords } from '../../enumerations'
import { GetContentRequestModel } from '../../models/requests'
import { GetContentResponseModel } from '../../models/responses'
import { Get } from '../../restClient'
export interface ContentState extends GetContentResponseModel {
// Interfaces
import { IHeader, IParams, IRequest, IResponse } from '../../interfaces'
import { IRoute } from '../../App'
import { INavMenuItem } from '../../layouts/public/NavMenu'
import { ISideMenuItem } from '../../layouts/admin/SideMenu'
import { IHomePage } from '../../pages/Home'
import { IShopCatalogPage } from '../../pages/Shop/Catalog'
import { IShopItemPage } from '../../pages/Shop/Item'
import { IBlogCatalogPage } from '../../pages/Blog/Catalog'
import { IBlogItemPage } from '../../pages/Blog/Item'
import { ISigninPage } from '../../pages/Signin'
import { ISignupPage } from '../../pages/Signup'
import { IShopCartPage } from '../../pages/Shop/Cart'
import { IShopCheckoutPage } from '../../pages/Shop/Checkout'
import { Get } from '../../restClient'
import { cloneObject } from '../../functions'
// Request
interface IGetContentPathParams extends IParams {}
interface IGetContentSearchParams extends IParams {
locale?: string
}
interface IGetContentRequestModel extends IRequest<IGetContentPathParams, IGetContentSearchParams> { }
interface ILocalization {
timeZone: string,
locale: string,
dateFormat: string,
timeFormat: string,
currency: string,
currencySymbol: string
}
// Response
interface IGetContentResponseModel extends IResponse {
siteName: string,
siteUrl: string,
header: IHeader,
localization: ILocalization,
routes: IRoute [],
adminRoutes: IRoute [],
serviceRoutes: IRoute [],
topMenu: INavMenuItem [],
sideMenu: ISideMenuItem [],
homePage: IHomePage,
shopCatalog: IShopCatalogPage,
shopItem: IShopItemPage,
shopCart: IShopCartPage,
shopCheckout: IShopCheckoutPage,
blogCatalog: IBlogCatalogPage,
blogItem: IBlogItemPage,
signIn: ISigninPage,
signUp: ISignupPage
}
export interface ContentState extends IGetContentResponseModel {
isLoading: boolean
}
interface RequestAction extends GetContentRequestModel {
interface RequestAction extends IGetContentRequestModel {
type: 'REQUEST_CONTENT'
}
interface ReceiveAction extends GetContentResponseModel {
interface ReceiveAction extends IGetContentResponseModel {
type: 'RECEIVE_CONTENT'
}
type KnownAction = RequestAction | ReceiveAction;
export const actionCreators = {
requestContent: (props?: GetContentRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
Get<Promise<GetContentResponseModel>>(`${process.env.REACT_APP_API}/Image/${process.env.REACT_APP_CONTENT}/${process.env.REACT_APP_SITEID}`, props?.pathParams, props?.searchParams)
.then(response => response)
.then((data) => {
if(data) {
dispatch({ type: 'RECEIVE_CONTENT', ...data })
}
})
dispatch({ type: 'REQUEST_CONTENT' })
}
}
const unloadedState: ContentState = {
siteName: "MAKS-IT",
siteUrl: "https://maks-it.com",
const mockData: IGetContentResponseModel = {
siteName: "Contoso",
siteUrl: "https://contoso.com",
header: {
title: `${ReservedWords.siteName}`,
@ -57,29 +115,42 @@ const unloadedState: ContentState = {
currency: "EUR",
currencySymbol: "€"
},
routes: [
{ 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: ":category", childRoutes: [
{ target: "", component: "ShopCatalog" },
{ target: ":page", component: "ShopCatalog" }
] },
{ target: ":category", childRoutes: [
{ target: ":page" , childRoutes: [
{ target: ":slug", component: "ShopItem" }
]}
] },
{ target:"cart", childRoutes: [
{ target: "", component: "Cart" },
{ target: "checkout", component: "Checkout" }
]}
]},
{ target: "/blog", childRoutes: [
{ target: "", component: "BlogCatalog" },
{ target: ":page", component: "BlogCatalog" },
{ target: ":page" , childRoutes: [
{ target: ":slug", component: "BlogItem" }
]}
{ target: ":category", childRoutes: [
{ target: "", component: "BlogCatalog" },
{ target: ":page", component: "BlogCatalog" }
] },
{ target: ":category", childRoutes: [
{ target: ":page" , childRoutes: [
{ target: ":slug", component: "BlogItem" }
]}
] }
]}
],
adminRoutes: [],
serviceRoutes: [
{ target: "/signin", component: "Signin" },
{ target: "/signup", component: "Signup" }
@ -87,14 +158,15 @@ const unloadedState: ContentState = {
topMenu: [
{ target: "/", title: "Home" },
{ target: "/shop", title: "Shop" },
{ target: "/blog", title: "Blog" },
{ target: "/shop/default", title: "Shop" },
{ target: "/blog/default", title: "Blog" },
{ target: "/signin", title: "Sing in" },
{ target: "/signup", title: "Sign up" },
{ target: "/shop/cart", icon: "shopping-cart", title: `Cart (${ReservedWords.quantity})` }
],
sideMenu: [],
homePage: {
@ -141,8 +213,7 @@ const unloadedState: ContentState = {
{
text: "The <code>ClientApp</code> subdirectory is a standard React application based on the <code>create-react-app</code> template. If you open a command prompt in that directory, you can run <code>yarn</code> commands such as <code>yarn test</code> or <code>yarn install</code>.",
reviewer: {
id: "",
image: { src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`, alt: "..." },
fullName: "Admin",
position: "CEO, MAKS-IT"
}
@ -150,7 +221,9 @@ const unloadedState: ContentState = {
]
},
featuredBlogsSection: {
title: "Featured blogs"
title: "Featured blogs",
text: "Chek our best blog posts",
readTime: `${ReservedWords.date} &middot; Time to read: ${ReservedWords.readTime} min`
},
callToActionSection: {
title: "New products, delivered to you.",
@ -199,6 +272,9 @@ const unloadedState: ContentState = {
relatedProductsSection: {
title: "Related products",
addToCart: "Add to cart"
},
commentsSection: {
leaveComment: "Join the discussion and leave a comment!"
}
},
@ -371,6 +447,9 @@ const unloadedState: ContentState = {
title: "Welcome to Blog Home!",
text: "A Bootstrap 5 starter layout for your next blog homepage"
},
blogItemsSection: {
readMore: 'Read more →'
},
featuredBlogSection: {
readTime: `${ReservedWords.date} Time to read: ${ReservedWords.readTime} min`
},
@ -387,7 +466,7 @@ const unloadedState: ContentState = {
}
},
titleSection: {
postedOnBy: `Posted on ${ReservedWords.date} by ${ReservedWords.nickName}`
text: `Posted on ${ReservedWords.date} by ${ReservedWords.nickName}`
},
commentsSection: {
leaveComment: "Join the discussion and leave a comment!"
@ -461,14 +540,42 @@ const unloadedState: ContentState = {
submit: {
title: "Sing up"
}
},
}
}
export const actionCreators = {
requestContent: (props?: IGetContentRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
dispatch({ type: 'REQUEST_CONTENT' })
if(process.env.REACT_APP_LOCAL_ONLY == 'Y') {
dispatch({ type: 'RECEIVE_CONTENT', ...cloneObject(mockData) })
return
}
const siteId = process.env.REACT_APP_SITEID
const pathParams = { ...props?.pathParams, siteId }
const locale = process.env.REACT_APP_LOCALE
const searchParams = { ...props?.searchParams, locale }
Get<Promise<IGetContentResponseModel>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_CONTENT}`, pathParams, searchParams)
.then(response => response)
.then((data) => {
if(data) {
dispatch({ type: 'RECEIVE_CONTENT', ...data })
}
})
}
}
const unloadedState: ContentState = {
...cloneObject(mockData),
isLoading: false
}
export const reducer: Reducer<ContentState> = (state: ContentState | undefined, incomingAction: Action): ContentState => {
if (state === undefined) {
console.log(unloadedState)
return unloadedState
}

View File

@ -2,19 +2,19 @@ import { Action, Reducer } from 'redux'
import { AppThunkAction } from '..'
import { ReservedWords } from '../../enumerations'
import { HeaderLink, HeaderModel } from '../../models'
import { IHeader } from '../../interfaces'
// -----------------
// STATE - This defines the type of data maintained in the Redux store.
export interface HeaderState extends HeaderModel {}
export interface HeaderState extends IHeader {}
// -----------------
// ACTIONS - These are serializable (hence replayable) descriptions of state transitions.
// They do not themselves have any side-effects they just describe something that is going to happen.
// Use @typeName and isActionType for type detection that works even after serialization/deserialization.
export interface ReceiveAction extends HeaderModel { type: 'RECEIVE_UPDATE_HEADER' }
export interface ReceiveAction extends IHeader { type: 'RECEIVE_UPDATE_HEADER' }
// Declare a 'discriminated union' type. This guarantees that all references to 'type' properties contain one of the
@ -27,7 +27,7 @@ export type KnownAction = ReceiveAction
export const actionCreators = {
updateHeader: (props: HeaderModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
updateHeader: (props: IHeader): AppThunkAction<KnownAction> => (dispatch, getState) => {
const siteName = getState().content?.siteName
const baseHeader = getState().content?.header

View File

@ -1,52 +0,0 @@
import { Action, Reducer } from 'redux'
// -----------------
// STATE - This defines the type of data maintained in the Redux store.
export interface LoaderState {
visible: boolean
}
interface RequestAction {
type: 'SHOW_LOADER'
}
interface ReceiveAction {
type: 'HIDE_LOADER'
}
// Declare a 'discriminated union' type. This guarantees that all references to 'type' properties contain one of the
// declared type strings (and not any other arbitrary string).
export type KnownAction = RequestAction | ReceiveAction
// ----------------
// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
// They don't directly mutate state, but they can have external side-effects (such as loading data).
export const actionCreators = {
show: () => ({ type: 'SHOW_LOADER' } as RequestAction),
hide: () => ({ type: 'HIDE_LOADER' } as ReceiveAction)
}
// ----------------
// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
const unloadedState: LoaderState = {
visible: false
}
export const reducer: Reducer<LoaderState> = (state: LoaderState | undefined, incomingAction: Action): LoaderState => {
if (state === undefined) {
return unloadedState
}
const action = incomingAction as KnownAction
switch (action.type) {
case 'SHOW_LOADER':
return { visible: true }
case 'HIDE_LOADER':
return { visible: false }
}
return state
}

View File

@ -1,22 +1,35 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '..'
import { GetShopCartItemsRequestModel } from '../../models/requests'
import { GetShopCartItemResponseModel } from '../../models/responses'
// Interfaces
import { IPagination, IParams, IRequest, IResponse } from '../../interfaces'
import { IShopCartItem } from '../../pages/Shop/Cart'
import { Get } from '../../restClient'
export interface ShopCartState {
items: GetShopCartItemResponseModel [],
// Request
interface IGetShopCartItemsPathParams extends IParams {
userId: string
}
interface IGetShopCartItemsSearchParams extends IParams { }
interface IGetShopCartItemsRequestModel extends IRequest<IGetShopCartItemsPathParams, IGetShopCartItemsSearchParams> {}
// Response
interface IGetShopCartItemsResponseModel extends IPagination<IShopCartItem>, IResponse {}
export interface ShopCartState extends IGetShopCartItemsResponseModel {
isLoading: boolean
}
export interface RequestAction {
export interface RequestAction extends IGetShopCartItemsRequestModel {
type: 'REQUEST_CART_ITEMS'
}
export interface ReceiveAction {
items: GetShopCartItemResponseModel [],
export interface ReceiveAction extends IGetShopCartItemsResponseModel {
type: 'RECEIVE_CART_ITEMS'
}
@ -31,14 +44,16 @@ export interface ReceiveAction {
export type KnownAction = RequestAction | ReceiveAction //| IncrementItemQuantityAction | DecrementItmeQuantityAction
export const actionCreators = {
requestCart: (props?: GetShopCartItemsRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
requestCart: (props?: IGetShopCartItemsRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
if(process.env.REACT_APP_LOCAL_ONLY == 'Y')
return
Get<Promise<GetShopCartItemResponseModel []>>('https://localhost:7151/api/ShopCartItems/404c8232-9048-4519-bfba-6e78dc7005ca', props?.pathParams, props?.searchParams)
Get<Promise<IGetShopCartItemsResponseModel>>('https://localhost:7151/api/ShopCartItems/404c8232-9048-4519-bfba-6e78dc7005ca', props?.pathParams, props?.searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_CART_ITEMS', items: [...data] })
dispatch({ type: 'RECEIVE_CART_ITEMS', ...data })
})
dispatch({ type: 'REQUEST_CART_ITEMS' })
@ -75,11 +90,13 @@ export const actionCreators = {
}
const unloadedState: ShopCartState = {
totalPages: 1,
currentPage: 1,
items: [
{
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
title: "Shop item title",
brandName: "Brand & Name",
shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...",
@ -91,7 +108,7 @@ const unloadedState: ShopCartState = {
{
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
title: "Shop item title",
brandName: "Brand & Name",
shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...",
@ -103,7 +120,7 @@ const unloadedState: ShopCartState = {
{
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
title: "Shop item title",
brandName: "Brand & Name",
shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...",

View File

@ -1,9 +1,28 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { GetShopCatalogRequestModel } from '../../models/requests'
import { GetShopCatalogResponseModel } from '../../models/responses'
// Interfaces
import { IPagination, IParams, IRequest, IResponse } from '../../interfaces'
import { IShopItem } from '../../pages/Shop/Catalog/ShopItemsSection'
import { Get } from '../../restClient'
import { cloneObject } from '../../functions'
// Request
interface IGetShopcatalogPathParams extends IParams {}
interface IGeShopCatalogSearchParams extends IParams {}
interface GetShopCatalogRequestModel extends IRequest<IGetShopcatalogPathParams, IGeShopCatalogSearchParams> {
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
// Response
interface GetShopCatalogResponseModel extends IPagination<IShopItem>, IResponse {}
export interface ShopCatalogState extends GetShopCatalogResponseModel {
isLoading: boolean
@ -19,33 +38,15 @@ interface ReceiveAction extends GetShopCatalogResponseModel {
type KnownAction = RequestAction | ReceiveAction
export const actionCreators = {
requestShopCatalog: (props?: GetShopCatalogRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
const locale = getState().content?.localization.locale
const searchParams = { ...props?.searchParams, locale }
Get<Promise<GetShopCatalogResponseModel>>(`${process.env.REACT_APP_API}/Image/${process.env.REACT_APP_SHOPITEMS}/${process.env.REACT_APP_SITEID}`, props?.pathParams, searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_SHOP_CATALOG', ...data })
})
dispatch({ type: 'REQUEST_SHOP_CATALOG' })
}
}
const unloadedState: ShopCatalogState = {
totalPages: 1,
const mockData: GetShopCatalogResponseModel = {
totalPages: 100,
currentPage: 1,
items: [
{
id: '',
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
badges: [ "sale" ],
title: "Shop item title",
brandName: "Brand & Name",
@ -54,7 +55,7 @@ const unloadedState: ShopCatalogState = {
text: "",
author: {
id: '',
image: { src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`, alt: "..." },
nickName: "Admin"
},
created: (new Date).toString(),
@ -69,7 +70,7 @@ const unloadedState: ShopCatalogState = {
id: '',
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
badges: [ "sale" ],
title: "Shop item title",
brandName: "Brand & Name",
@ -78,7 +79,7 @@ const unloadedState: ShopCatalogState = {
text: "",
author: {
id: '',
image: { src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`, alt: "..." },
nickName: "Admin"
},
created: (new Date).toString(),
@ -93,7 +94,7 @@ const unloadedState: ShopCatalogState = {
id: '',
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
badges: [ "sale", "out of stock" ],
title: "Shop item title",
brandName: "Brand & Name",
@ -102,7 +103,7 @@ const unloadedState: ShopCatalogState = {
text: "",
author: {
id: '',
image: { src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`, alt: "..." },
nickName: "Admin"
},
created: (new Date).toString(),
@ -117,7 +118,7 @@ const unloadedState: ShopCatalogState = {
id: '',
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
badges: [ "sale" ],
title: "Shop item title",
brandName: "Brand & Name",
@ -126,7 +127,7 @@ const unloadedState: ShopCatalogState = {
text: "",
author: {
id: '',
image: { src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`, alt: "..." },
nickName: "Admin"
},
created: (new Date).toString(),
@ -137,7 +138,32 @@ const unloadedState: ShopCatalogState = {
price: 20,
newPrice: 10
}
],
]
}
export const actionCreators = {
requestShopCatalog: (props?: GetShopCatalogRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
dispatch({ type: 'REQUEST_SHOP_CATALOG' })
const locale = process.env.REACT_APP_LOCALE
const searchParams = { ...props?.searchParams, locale }
if(process.env.REACT_APP_LOCAL_ONLY == 'Y') {
dispatch({ type: 'RECEIVE_SHOP_CATALOG', ...cloneObject(mockData) })
return
}
Get<Promise<GetShopCatalogResponseModel>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_SITEID}/${process.env.REACT_APP_SHOPITEMS}`, props?.pathParams, searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_SHOP_CATALOG', ...data })
})
}
}
const unloadedState: ShopCatalogState = {
...cloneObject(mockData),
isLoading: false
}

View File

@ -1,9 +1,24 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { GetShopCategoriesRequestModel } from '../../models/requests'
import { GetShopCategoriesResponseModel } from '../../models/responses'
// Interfaces
import { IPagination, IParams, IRequest, IResponse } from '../../interfaces'
import { ICategory } from '../../components/SideWidgets/Categories'
import { Get } from '../../restClient'
import { cloneObject } from '../../functions'
// Request
interface GetShopCategoriesPathParams extends IParams { }
interface GetShopCategoriesSearchParams extends IParams { }
interface GetShopCategoriesRequestModel extends IRequest<GetShopCategoriesPathParams, GetShopCategoriesSearchParams> { }
// Response
interface GetShopCategoriesResponseModel extends IPagination<ICategory>, IResponse { }
export interface ShopCategoriesState extends GetShopCategoriesResponseModel {
isLoading: boolean
@ -19,25 +34,36 @@ interface ReceiveAction extends GetShopCategoriesResponseModel {
type KnownAction = RequestAction | ReceiveAction
const mockData: GetShopCategoriesResponseModel = {
totalPages: 1,
currentPage: 1,
items: [
{ href: 'default', anchorText: "Default" },
{ href: 'software', anchorText: "Software" },
{ href: 'hardware', anchorText: "Hardware" }
]
}
export const actionCreators = {
requestShopCategories: (props?: GetShopCategoriesRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
dispatch({ type: 'REQUEST_SHOP_CATEGORIES' })
Get<Promise<GetShopCategoriesResponseModel>>('https://localhost:7151/api/ShopCategories', props?.pathParams, props?.searchParams)
if(process.env.REACT_APP_LOCAL_ONLY == 'Y') {
dispatch({ type: 'RECEIVE_SHOP_CATEGORIES', ...cloneObject(mockData) })
return
}
Get<Promise<GetShopCategoriesResponseModel>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_CATEGORYITEMS}/${process.env.REACT_APP_SITEID}`, props?.pathParams, props?.searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_SHOP_CATEGORIES', ...data })
})
dispatch({ type: 'REQUEST_SHOP_CATEGORIES' })
}
}
const unloadedState: ShopCategoriesState = {
items: [
{ id: "", text: "Software" },
{ id: "", text: "Hardware" }
],
...cloneObject(mockData),
isLoading: false
}

View File

@ -1,28 +1,79 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { GetShopFeaturedRequestModel, } from '../../models/requests'
import { GetShopFeaturedResponseModel } from '../../models/responses'
import { Get } from '../../restClient'
// Interfaces
import { IParams, IRequest, IResponse } from '../../interfaces'
import { IRelatedProduct, IRelatedProducts } from '../../pages/Shop/Item/RelatedProducts'
export interface ShopFeaturedState extends GetShopFeaturedResponseModel {
import { Get } from '../../restClient'
import { cloneObject } from '../../functions'
// Request
interface IGetShopFeaturedSearchParams extends IParams { }
interface IGetShopFeaturedPathParams extends IParams { }
interface IGetShopFeaturedRequestModel extends IRequest<IGetShopFeaturedSearchParams, IGetShopFeaturedPathParams> { }
// Response
interface IGetShopFeaturedResponseModel extends IResponse {
items: IRelatedProduct []
}
export interface ShopFeaturedState extends IGetShopFeaturedResponseModel {
isLoading: boolean
}
interface RequestAction extends GetShopFeaturedRequestModel {
interface RequestAction extends IGetShopFeaturedRequestModel {
type: 'REQUEST_SHOP_FEATURED'
}
interface ReceiveAction extends GetShopFeaturedResponseModel {
interface ReceiveAction extends IGetShopFeaturedResponseModel {
type: 'RECEIVE_SHOP_FEATURED'
}
type KnownAction = RequestAction | ReceiveAction
export const actionCreators = {
requestShopFeatured: (props?: GetShopFeaturedRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
const mockData: IGetShopFeaturedResponseModel = {
items: [
{
id: '',
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
badges: [ "sale" ],
title: "Shop item title",
brandName: "Brand & Name",
Get<Promise<GetShopFeaturedResponseModel>>('https://localhost:7151/api/ShopFeatured', props?.pathParams, props?.searchParams)
shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...",
text: "",
author: {
id: '',
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`, alt: "..." },
nickName: "Admin"
},
created: (new Date).toString(),
tags: [ "react", "redux", "webapi" ],
rating: 4.5,
price: 20,
newPrice: 10
}
]
}
export const actionCreators = {
requestShopFeatured: (props?: IGetShopFeaturedRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
if(process.env.REACT_APP_LOCAL_ONLY == 'Y') {
dispatch({ type: 'RECEIVE_SHOP_FEATURED', ...cloneObject(mockData) })
return
}
Get<Promise<IGetShopFeaturedResponseModel>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_SHOPITEMS}/${process.env.REACT_APP_SITEID}`, props?.pathParams, props?.searchParams)
.then(response => response)
.then(data => {
if(data)
@ -34,32 +85,7 @@ export const actionCreators = {
}
const unloadedState: ShopFeaturedState = {
items: [
{
id: '',
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
badges: [ "sale" ],
title: "Shop item title",
brandName: "Brand & Name",
shortText: "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...",
text: "",
author: {
id: '',
image: { src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`, alt: "..." },
nickName: "Admin"
},
created: (new Date).toString(),
tags: [ "react", "redux", "webapi" ],
rating: 4.5,
price: 20,
newPrice: 10
}
],
...cloneObject(mockData),
isLoading: false
}

View File

@ -1,9 +1,26 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { GetShopItemRequestModel } from '../../models/requests'
import { GetShopItemResponseModel } from '../../models/responses'
// Interfaces
import { IParams, IRequest, IResponse } from '../../interfaces'
import { IShopItem } from '../../pages/Shop/Item'
import { Get } from '../../restClient'
import { cloneObject } from '../../functions'
// Reuest
interface IGetShopItemPathParams extends IParams { }
interface IGetShopItemSearchParams extends IParams {
slug: string
}
interface GetShopItemRequestModel extends IRequest<IGetShopItemPathParams, IGetShopItemSearchParams> { }
// Response
interface GetShopItemResponseModel extends IShopItem, IResponse { }
export interface ShopItemState extends GetShopItemResponseModel {
@ -20,25 +37,11 @@ interface ReceiveAction extends GetShopItemResponseModel {
type KnownAction = RequestAction | ReceiveAction
export const actionCreators = {
requestShopItem: (props?: GetShopItemRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
Get<Promise<GetShopItemResponseModel>>('https://localhost:7151/api/ShopItem', props?.pathParams, props?.searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_SHOP_ITEM', ...data })
})
// dispatch({ type: 'REQUEST_SHOP_ITEM', slug: props.slug })
}
}
const unloadedState: ShopItemState = {
const mockData: GetShopItemResponseModel = {
id: "",
slug: "demo-post",
image: {
src: `${process.env.REACT_APP_API}/Image/600x700/dee2e6/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/600x700/dee2e6/6c757d`,
alt: "..."
},
@ -56,7 +59,7 @@ const unloadedState: ShopItemState = {
id: "",
nickName: "Admin",
image: {
src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`,
src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`,
alt: "..."
}
},
@ -66,58 +69,29 @@ const unloadedState: ShopItemState = {
price: 20,
newPrice: 10,
quantity: 10,
comments: [
{
author: {
id: "",
nickName: "Commenter Name 1",
image: {
src: `${process.env.REACT_APP_API}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "If you're going to lead a space frontier, it has to be government; it'll never be private enterprise. Because the space frontier is dangerous, and it's expensive, and it has unquantified risks.",
responses: [
{
author: {
id: "",
nickName: "Commenter Name 4",
image: {
src: `${process.env.REACT_APP_API}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "And under those conditions, you cannot establish a capital-market evaluation of that enterprise. You can't get investors."
},
{
author: {
id: "",
nickName: "Commenter Name 3",
image: {
src: `${process.env.REACT_APP_API}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "When you put money directly to a problem, it makes a good headline."
}
]
},
{
author: {
id: "",
nickName: "Commenter Name 2",
image: {
src: `${process.env.REACT_APP_API}/Image/50x50/ced4da/6c757d`,
alt: "..."
}
},
comment: "When I look at the universe and all the ways the universe wants to kill us, I find it hard to reconcile that with statements of beneficence."
quantity: 10
}
export const actionCreators = {
requestShopItem: (props?: GetShopItemRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
dispatch({ type: 'REQUEST_SHOP_ITEM' })
if(process.env.REACT_APP_LOCAL_ONLY == 'Y') {
dispatch({ type: 'RECEIVE_SHOP_ITEM', ...cloneObject(mockData) })
return
}
],
Get<Promise<GetShopItemResponseModel>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_SHOPITEM}/${process.env.REACT_APP_SITEID}`, props?.pathParams, props?.searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_SHOP_ITEM', ...data })
})
}
}
const unloadedState: ShopItemState = {
...cloneObject(mockData),
isLoading: false
}

View File

@ -1,45 +1,46 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
import { GetShopRelatedRequestModel } from '../../models/requests'
import { GetShopRelatedResponseModel } from '../../models/responses'
import { Get } from '../../restClient'
import { IParams, IRequest, IResponse } from '../../interfaces'
import { IRelatedProduct, IRelatedProducts } from '../../pages/Shop/Item/RelatedProducts'
export interface ShopRelatedState extends GetShopRelatedResponseModel {
import { Get } from '../../restClient'
import { cloneObject } from '../../functions'
// Resquest
interface IGetShopRelatedPathParams extends IParams {}
interface IGetShopRelatedSearchParams extends IParams {}
interface IGetShopRelatedRequestModel extends IRequest<IGetShopRelatedPathParams, IGetShopRelatedSearchParams> { }
// Response
interface IGetShopRelatedResponseModel extends IResponse {
items: IRelatedProduct []
}
export interface ShopRelatedState extends IGetShopRelatedResponseModel {
isLoading: boolean
}
interface RequestAction extends GetShopRelatedRequestModel {
interface RequestAction extends IGetShopRelatedRequestModel {
type: 'REQUEST_SHOP_RELATED'
}
interface ReceiveAction extends GetShopRelatedResponseModel {
interface ReceiveAction extends IGetShopRelatedResponseModel {
type: 'RECEIVE_SHOP_RELATED'
}
type KnownAction = RequestAction | ReceiveAction
export const actionCreators = {
requestShopRelated: (props?: GetShopRelatedRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
Get<Promise<GetShopRelatedResponseModel>>('https://localhost:7151/api/ShopRelated', props?.pathParams, props?.searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_SHOP_RELATED', ...data })
})
dispatch({ type: 'REQUEST_SHOP_RELATED' })
}
}
const unloadedState: ShopRelatedState = {
const mockData: IGetShopRelatedResponseModel = {
items: [
{
id: '',
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
badges: [ "sale", "best offer" ],
title: "Shop item title",
brandName: "Brand & Name",
@ -48,7 +49,7 @@ const unloadedState: ShopRelatedState = {
text: "",
author: {
id: '',
image: { src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`, alt: "..." },
nickName: "Admin"
},
created: (new Date).toString(),
@ -63,7 +64,7 @@ const unloadedState: ShopRelatedState = {
id: '',
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
badges: [ "sale", "best offer" ],
title: "Shop item title",
brandName: "Brand & Name",
@ -72,7 +73,7 @@ const unloadedState: ShopRelatedState = {
text: "",
author: {
id: '',
image: { src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`, alt: "..." },
nickName: "Admin"
},
created: (new Date).toString(),
@ -87,7 +88,7 @@ const unloadedState: ShopRelatedState = {
id: '',
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
badges: [ "sale", "best offer" ],
title: "Shop item title",
brandName: "Brand & Name",
@ -96,7 +97,7 @@ const unloadedState: ShopRelatedState = {
text: "",
author: {
id: '',
image: { src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`, alt: "..." },
nickName: "Admin"
},
created: (new Date).toString(),
@ -111,7 +112,7 @@ const unloadedState: ShopRelatedState = {
id: '',
slug: "shop-catalog-item",
sku: "SKU-0",
image: { src: `${process.env.REACT_APP_API}/Image/450x300/dee2e6/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/450x300/dee2e6/6c757d`, alt: "..." },
badges: [ "sale", "best offer" ],
title: "Shop item title",
brandName: "Brand & Name",
@ -120,7 +121,7 @@ const unloadedState: ShopRelatedState = {
text: "",
author: {
id: '',
image: { src: `${process.env.REACT_APP_API}/Image/40x40/ced4da/6c757d`, alt: "..." },
image: { src: `${process.env.REACT_APP_FRONTEND}/Image/40x40/ced4da/6c757d`, alt: "..." },
nickName: "Admin"
},
created: (new Date).toString(),
@ -131,7 +132,29 @@ const unloadedState: ShopRelatedState = {
price: 20,
newPrice: 10
}
],
]
}
export const actionCreators = {
requestShopRelated: (props?: IGetShopRelatedRequestModel): AppThunkAction<KnownAction> => (dispatch, getState) => {
dispatch({ type: 'REQUEST_SHOP_RELATED' })
if(process.env.REACT_APP_LOCAL_ONLY == 'Y') {
dispatch({ type: 'RECEIVE_SHOP_RELATED', ...cloneObject(mockData) })
return
}
Get<Promise<IGetShopRelatedResponseModel>>(`${process.env.REACT_APP_API}/${process.env.REACT_APP_SITEID}/${process.env.REACT_APP_SHOPITEMS}`, props?.pathParams, props?.searchParams)
.then(response => response)
.then(data => {
if(data)
dispatch({ type: 'RECEIVE_SHOP_RELATED', ...data })
})
}
}
const unloadedState: ShopRelatedState = {
...cloneObject(mockData),
isLoading: false
}

View File

@ -1,6 +1,17 @@
import { Action, Reducer } from 'redux'
import { AppThunkAction } from '../'
// export interface GetWeatherForecastResponseModel extends ResponseModel {
// date: string,
// temperatireC: number,
// temperatureF: number,
// summary?: string
// }
// -----------------
// STATE - This defines the type of data maintained in the Redux store.

View File

@ -4,14 +4,14 @@ using DomainResults.Common;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using DomainObjects.Documents;
using DataProviders.Collections.Abstractions;
using DomainObjects.Documents.Content;
namespace DataProviders.Collections
{
public interface IContentDataProvider {
(List<ContentDocument>?, IDomainResult) Get(Guid siteId);
(ContentDocument?, IDomainResult) Get(Guid siteId);
}
public class ContentDataProvider : CollectionDataProviderBase<ContentDocument>, IContentDataProvider {
@ -25,6 +25,13 @@ namespace DataProviders.Collections
ISessionService sessionService) : base(logger, client, idGenerator, sessionService, _databaseName, _collectionName) {
}
public (List<ContentDocument>?, IDomainResult) Get(Guid siteId) => GetWithPredicate(x => x.SiteId == siteId, x => x);
public (ContentDocument?, IDomainResult) Get(Guid siteId) {
var (list, result) = GetWithPredicate(x => x.Id == siteId, x => x);
if (!result.IsSuccess || list == null)
return (null, result);
return (list.First(), result);
}
}
}

View File

@ -15,6 +15,10 @@ using DomainObjects.Documents.Sites;
using DomainObjects.Documents.Categories;
using DomainObjects.Documents.Categories.L10n;
using DomainObjects.Documents.Posts;
using DomainObjects.Documents.Content;
using DomainObjects.Abstractions.Posts.L10n;
using DomainObjects.Documents.Content.L10n;
using Core.Enumerations;
namespace DataProviders
{
@ -32,19 +36,8 @@ namespace DataProviders
// https://kevsoft.net/2020/07/02/how-to-store-decimal-fields-in-mongodb-with-csharp.html
BsonSerializer.RegisterSerializer(new DecimalSerializer(BsonType.Decimal128));
#region L10n
if (!BsonClassMap.IsClassMapRegistered(typeof(PostItemL10n))) {
BsonClassMap.RegisterClassMap<PostItemL10n>(cm => {
cm.AutoMap();
cm.GetMemberMap(c => c.Locale)
.SetSerializer(new EnumerationSerializer<Locales>());
cm.GetMemberMap(c => c.TextFormat)
.SetSerializer(new EnumerationSerializer<TextFormat>());
});
}
#region MediaAttachments
if (!BsonClassMap.IsClassMapRegistered(typeof(MediaAttachmentL10n))) {
BsonClassMap.RegisterClassMap<MediaAttachmentL10n>(cm => {
cm.AutoMap();
@ -54,6 +47,21 @@ namespace DataProviders
});
}
if (!BsonClassMap.IsClassMapRegistered(typeof(MediaAttachment))) {
BsonClassMap.RegisterClassMap<MediaAttachment>(cm => {
cm.AutoMap();
cm.GetMemberMap(c => c.MediaType)
.SetSerializer(new EnumerationSerializer<MediaTypes>());
});
}
#endregion
#region L10n
if (!BsonClassMap.IsClassMapRegistered(typeof(CategoryL10n))) {
BsonClassMap.RegisterClassMap<CategoryL10n>(cm => {
cm.AutoMap();
@ -118,14 +126,7 @@ namespace DataProviders
});
}
if (!BsonClassMap.IsClassMapRegistered(typeof(Localization))) {
BsonClassMap.RegisterClassMap<Localization>(cm => {
cm.AutoMap();
cm.GetMemberMap(c => c.Locale)
.SetSerializer(new EnumerationSerializer<Locales>());
});
}
if (!BsonClassMap.IsClassMapRegistered(typeof(MenuItem))) {
BsonClassMap.RegisterClassMap<MenuItem>(cm => {
@ -158,15 +159,25 @@ namespace DataProviders
#endregion
#region BlogItem
#region Blog and Shop item
if (!BsonClassMap.IsClassMapRegistered(typeof(PostItemL10n))) {
BsonClassMap.RegisterClassMap<PostItemL10n>(cm => {
cm.AutoMap();
cm.GetMemberMap(c => c.Locale)
.SetSerializer(new EnumerationSerializer<Locales>());
cm.GetMemberMap(c => c.TextFormat)
.SetSerializer(new EnumerationSerializer<TextFormat>());
});
}
if (!BsonClassMap.IsClassMapRegistered(typeof(BlogDocument))) {
BsonClassMap.RegisterClassMap<BlogDocument>(cm => {
cm.AutoMap();
});
}
#endregion
#region ShopItem
if (!BsonClassMap.IsClassMapRegistered(typeof(ShopDocument))) {
BsonClassMap.RegisterClassMap<ShopDocument>(cm => {
cm.AutoMap();
@ -331,6 +342,21 @@ namespace DataProviders
#region Content
if (!BsonClassMap.IsClassMapRegistered(typeof(Settings))) {
BsonClassMap.RegisterClassMap<Settings>(cm => {
cm.AutoMap();
});
}
if (!BsonClassMap.IsClassMapRegistered(typeof(ContentL10n))) {
BsonClassMap.RegisterClassMap<ContentL10n>(cm => {
cm.AutoMap();
cm.GetMemberMap(c => c.Locale)
.SetSerializer(new EnumerationSerializer<Locales>());
});
}
if (!BsonClassMap.IsClassMapRegistered(typeof(ContentDocument))) {
BsonClassMap.RegisterClassMap<ContentDocument>(cm => {

View File

@ -2,5 +2,5 @@
public abstract class PersonBase<T> : DomainObjectBase<T> {
public Guid Id { get; set; }
public Image? Image { get; set; }
public MediaAttachment? Image { get; set; }
}

View File

@ -1,25 +0,0 @@
using DomainObjects.L10n;
namespace DomainObjects.Abstractions;
public abstract class PostItemBase<T> : DomainObjectDocumentBase<T> {
public Guid SiteId { get; set; }
public List<PostItemL10n> L10n { get; set; }
public List<MediaAttachment>? MediaAttachments { get; set; }
public Guid Author { get; set; }
public DateTime Created { get; set; }
public DateTime? Published { get; set; }
public List<string>? Tags { get; set; }
public List<Guid> Categories { get; set; }
public bool? FamilyFriendly { get; set; }
}

View File

@ -0,0 +1,37 @@
using DomainObjects.Enumerations;
namespace DomainObjects.Abstractions.Posts.L10n;
public class PostItemL10n : DomainObjectBase<PostItemL10n> {
public Locales Locale { get; set; }
public string Slug { get; set; }
public string Description { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public TextFormat TextFormat { get; set; }
public string PlainText { get; set; }
public string ShortText { get; set; }
public List<string>? Badges { get; set; }
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + Locale.GetHashCode();
hash = hash * 23 + Slug.GetHashCode();
hash = hash * 23 + Description.GetHashCode();
hash = hash * 23 + Title.GetHashCode();
hash = hash * 23 + Text.GetHashCode();
hash = hash * 23 + TextFormat.GetHashCode();
hash = hash * 23 + ShortText.GetHashCode();
if (Badges != null)
hash = hash * 23 + Badges.GetHashCode();
return hash;
}
}
}

View File

@ -0,0 +1,25 @@
using DomainObjects.Abstractions.Posts.L10n;
namespace DomainObjects.Abstractions.Posts;
public abstract class PostItemBase<T> : DomainObjectDocumentBase<T> {
public Guid SiteId { get; set; }
public List<PostItemL10n> L10n { get; set; }
public List<MediaAttachment>? MediaAttachments { get; set; }
public Guid Author { get; set; }
public DateTime Created { get; set; }
public DateTime? Published { get; set; }
public List<string>? Tags { get; set; }
public List<Guid> Categories { get; set; }
public bool? FamilyFriendly { get; set; }
}

View File

@ -0,0 +1,13 @@
using DomainObjects.Abstractions;
using DomainObjects.Documents.Content.L10n;
namespace DomainObjects.Documents.Content;
public class ContentDocument : DomainObjectDocumentBase<ContentDocument> {
public List<ContentL10n> L10n { get; set; }
public override int GetHashCode()
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,41 @@
using DomainObjects.Enumerations;
using DomainObjects.Pages;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DomainObjects.Documents.Content.L10n {
public class ContentL10n {
public Locales Locale { get; set; }
public string SiteName { get; set; }
public string SiteUrl { get; set; }
public Header Header { get; set; }
public Settings Settings { get; set; }
public List<Route> Routes { get; set; }
public List<Route> AdminRoutes { get; set; }
public List<Route> ServiceRoutes { get; set; }
public List<MenuItem> TopMenu { get; set; }
public List<MenuItem> SideMenu { get; set; }
public HomePage HomePage { get; set; }
public ShopCatalogPage ShopCatalog { get; set; }
public ShopItemPage ShopItem { get; set; }
public ShopCartPage ShopCart { get; set; }
public ShopCheckoutPage ShopCheckout { get; set; }
public BlogCatalogPage BlogCatalog { get; set; }
public BlogItemPage BlogItem { get; set; }
public SignInPage SignIn { get; set; }
public SignUpPage SignUp { get; set; }
}
}

View File

@ -1,41 +0,0 @@
using DomainObjects.Abstractions;
using DomainObjects.Pages;
namespace DomainObjects.Documents;
public class ContentDocument : DomainObjectDocumentBase<ContentDocument> {
public Guid SiteId { get; set; }
public string SiteName { get; set; }
public string SiteUrl { get; set; }
public Header Header { get; set; }
public Localization Localization { get; set; }
public List<Route> Routes { get; set; }
public List<Route> AdminRoutes { get; set; }
public List<Route> ServiceRoutes { get; set; }
public List<MenuItem> TopMenu { get; set; }
public List<MenuItem> SideMenu { get; set; }
public HomePage HomePage { get; set; }
public ShopCatalogPage ShopCatalog { get; set; }
public ShopItemPage ShopItem { get; set; }
public ShopCartPage ShopCart { get; set; }
public ShopCheckoutPage ShopCheckout { get; set; }
public BlogCatalogPage BlogCatalog { get; set; }
public BlogItemPage BlogItem { get; set; }
public SignInPage SignIn { get; set; }
public SignUpPage SignUp { get; set; }
public override int GetHashCode() {
throw new NotImplementedException();
}
}

View File

@ -1,4 +1,4 @@
using DomainObjects.Abstractions;
using DomainObjects.Abstractions.Posts;
namespace DomainObjects.Documents.Posts;

View File

@ -1,4 +1,4 @@
using DomainObjects.Abstractions;
using DomainObjects.Abstractions.Posts;
namespace DomainObjects.Documents.Posts;

View File

@ -1,17 +0,0 @@
using DomainObjects.Abstractions;
namespace DomainObjects;
public class Image : DomainObjectBase<MediaAttachment> {
public string Src { get; set; }
public string Alt { get; set; }
public override int GetHashCode() {
int hash = 17;
hash = hash * 23 + Src.GetHashCode();
hash = hash * 23 + Alt.GetHashCode();
return hash;
}
}

View File

@ -1,36 +0,0 @@
using DomainObjects.Abstractions;
using DomainObjects.Enumerations;
namespace DomainObjects.L10n;
public class PostItemL10n : DomainObjectBase<PostItemL10n> {
public Locales Locale { get; set; }
public string Slug { get; set; }
public string Description { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public TextFormat TextFormat { get; set; }
public string PlainText { get; set; }
public string ShortText { get; set; }
public List<string>? Badges { get; set; }
public override int GetHashCode() {
unchecked {
int hash = 17;
hash = hash * 23 + Locale.GetHashCode();
hash = hash * 23 + Slug.GetHashCode();
hash = hash * 23 + Description.GetHashCode();
hash = hash * 23 + Title.GetHashCode();
hash = hash * 23 + Text.GetHashCode();
hash = hash * 23 + TextFormat.GetHashCode();
hash = hash * 23 + ShortText.GetHashCode();
if(Badges != null)
hash = hash * 23 + Badges.GetHashCode();
return hash;
}
}
}

View File

@ -3,7 +3,7 @@
namespace DomainObjects.PageSections;
public class TitleSection : PageSectionBase<TitleSection> {
public Image? Image { get; set; }
public MediaAttachment? Image { get; set; }
public Link? PrimaryLink { get; set; }
public Link? SecondaryLink { get; set; }
public string? PostedOnBy { get; set; }

View File

@ -3,12 +3,10 @@ using DomainObjects.Enumerations;
namespace DomainObjects;
public class Localization : DomainObjectBase<Localization> {
public class Settings : DomainObjectBase<Settings> {
public string? TimeZone { get; set; }
public Locales Locale { get; set; }
public string? DateFormat { get; set; }
public string? TimeFormat { get; set; }

View File

@ -82,5 +82,46 @@ namespace Extensions {
return false;
}
private static readonly string[] suffixes = { "Bytes", "KB", "MB", "GB", "TB", "PB" };
private static decimal FormatSize(int bytes, int index = 1) => (decimal)bytes / 1024 * index;
public static string ToKB(this int bytes) =>
string.Format("{0:n1}{1}", FormatSize(bytes, 1), suffixes[1]);
public static string ToKB(this byte[] bytes) =>
string.Format("{0:n1}{1}", FormatSize(bytes.Length, 1), suffixes[1]);
public static string ToMB(this int bytes) =>
string.Format("{0:n1}{1}", FormatSize(bytes, 2), suffixes[2]);
public static string ToMB(this byte[] bytes) =>
string.Format("{0:n1}{1}", FormatSize(bytes.Length, 2), suffixes[2]);
public static string ToGB(this int bytes) =>
string.Format("{0:n1}{1}", FormatSize(bytes, 3), suffixes[3]);
public static string ToGB(this byte[] bytes) =>
string.Format("{0:n1}{1}", FormatSize(bytes.Length, 3), suffixes[3]);
public static string ToTB(this int bytes) =>
string.Format("{0:n1}{1}", FormatSize(bytes, 4), suffixes[4]);
public static string ToTB(this byte[] bytes) =>
string.Format("{0:n1}{1}", FormatSize(bytes.Length, 4), suffixes[4]);
public static string ToPB(this int bytes) =>
string.Format("{0:n1}{1}", FormatSize(bytes, 5), suffixes[5]);
public static string ToPB(this byte[] bytes) =>
string.Format("{0:n1}{1}", FormatSize(bytes.Length, 5), suffixes[5]);
}
}

View File

@ -83,8 +83,6 @@ namespace ImageProvider {
image.Mutate(ctx => {
ctx.BackgroundColor(backgroundColor);
ctx.ApplyScalingWaterMark(font, $"{width}x{height}", foregroundColor, 5, false);
//x.Fill(, new Rectangle(0, 0, width, height));
//x.DrawText($"{width}x{height}", font, Color.Black, new PointF(10, 10));
});
using var stream = new MemoryStream();

View File

@ -18,7 +18,7 @@
"route2": {
"ClusterId": "cluster1",
"Match": {
"Path": "image/{**catchall}"
"Path": "Image/{**catchall}"
},
"Transforms": [
{ "PathPattern": "/api/Image/{**catchall}" }
@ -48,7 +48,7 @@
"cluster2": {
"Destinations": {
"destination1": {
"Address": "http://localhost:3000/"
"Address": "http://clientapp:3000/"
}
}
}

View File

@ -8,13 +8,11 @@ EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["WeatherForecast/WeatherForecast.csproj", "WeatherForecast/"]
COPY ["Services/FileSecurityService/FileSecurityService.csproj", "Services/FileSecurityService/"]
COPY ["FileSecurityService/FileSecurityService.csproj", "Services/FileSecurityService/"]
COPY ["Extensions/Extensions.csproj", "Extensions/"]
COPY ["Core/Core.csproj", "Core/"]
COPY ["Services/ImageProvider/ImageProvider.csproj", "Services/ImageProvider/"]
COPY ["ImageProvider/ImageProvider.csproj", "Services/ImageProvider/"]
COPY ["DataProviders/DataProviders.csproj", "DataProviders/"]
COPY ["Services/JWTService/JWTService.csproj", "Services/JWTService/"]
COPY ["Services/HashService/HashService.csproj", "Services/HashService/"]
RUN dotnet restore "WeatherForecast/WeatherForecast.csproj"
COPY . .
WORKDIR "/src/WeatherForecast"

View File

@ -7,6 +7,7 @@ using DomainObjects;
using Core.Enumerations;
using Core.Abstractions.Models;
using DomainObjects.Abstractions.Posts.L10n;
namespace WeatherForecast.Models.Blog.Requests {

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using DomainObjects.Abstractions.Posts.L10n;
using DomainObjects.Documents.Posts;
using DomainObjects.Enumerations;
using DomainObjects.L10n;
@ -8,6 +9,7 @@ using DomainObjects;
using Core.Enumerations;
using Core.Abstractions.Models;
namespace WeatherForecast.Models.Blog.Requests {
/// <summary>

Some files were not shown because too many files have changed in this diff Show More