Compare commits
10 Commits
5ad87b6d48
...
2bfc919feb
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bfc919feb | |||
| 619e16459c | |||
| 07061316ae | |||
| 12edae98bb | |||
| b2c7a68070 | |||
| abbbcac220 | |||
| e603ff41a1 | |||
| cb4615b14d | |||
| 740a5d2d75 | |||
| eb5334917d |
22
.gitlab-ci.yml
Normal file
22
.gitlab-ci.yml
Normal 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
23
Jenkinsfile
vendored
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -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 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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,3 +1,6 @@
|
||||
*
|
||||
!obj\Docker\publish\*
|
||||
!obj\Docker\empty\
|
||||
**/.dockerignore
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
!**/.env
|
||||
@ -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
23
src/ClientApp/.gitignore
vendored
Normal 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*
|
||||
22
src/ClientApp/.vscode/launch.json
vendored
22
src/ClientApp/.vscode/launch.json
vendored
@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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" />
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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/).
|
||||
|
||||
18734
src/ClientApp/package-lock.json
generated
18734
src/ClientApp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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 |
@ -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.
|
||||
|
||||
BIN
src/ClientApp/public/logo192.png
Normal file
BIN
src/ClientApp/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/ClientApp/public/logo512.png
Normal file
BIN
src/ClientApp/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
@ -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"
|
||||
|
||||
3
src/ClientApp/public/robots.txt
Normal file
3
src/ClientApp/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@ -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 }} />}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { FC, ReactNode, useEffect, useState } from 'react'
|
||||
import React, { FC, ReactNode } from 'react'
|
||||
|
||||
import './scss/loaders.scss'
|
||||
|
||||
|
||||
40
src/ClientApp/src/components/Modal/index.tsx
Normal file
40
src/ClientApp/src/components/Modal/index.tsx
Normal 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
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
10
src/ClientApp/src/functions/cloneObject.ts
Normal file
10
src/ClientApp/src/functions/cloneObject.ts
Normal 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
|
||||
}
|
||||
@ -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 => {
|
||||
|
||||
@ -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
|
||||
}
|
||||
7
src/ClientApp/src/functions/isSuccessStatusCode.ts
Normal file
7
src/ClientApp/src/functions/isSuccessStatusCode.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const isSuccessStatusCode = (statusCode: number) => {
|
||||
return statusCode >= 200 && statusCode <= 299
|
||||
}
|
||||
|
||||
export {
|
||||
isSuccessStatusCode
|
||||
}
|
||||
123
src/ClientApp/src/interfaces/index.ts
Normal file
123
src/ClientApp/src/interfaces/index.ts
Normal 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
|
||||
// }
|
||||
@ -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,
|
||||
|
||||
@ -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)}</>
|
||||
|
||||
@ -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 []
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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> {}
|
||||
@ -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
|
||||
}
|
||||
@ -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>
|
||||
}
|
||||
|
||||
108
src/ClientApp/src/pages/Blog/Catalog/BlogItemsComponent.tsx
Normal file
108
src/ClientApp/src/pages/Blog/Catalog/BlogItemsComponent.tsx
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
30
src/ClientApp/src/pages/Blog/Catalog/TitleComponent.tsx
Normal file
30
src/ClientApp/src/pages/Blog/Catalog/TitleComponent.tsx
Normal 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
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
40
src/ClientApp/src/pages/Blog/Item/TitleSection.tsx
Normal file
40
src/ClientApp/src/pages/Blog/Item/TitleSection.tsx
Normal 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
|
||||
}
|
||||
@ -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">
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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>()
|
||||
|
||||
|
||||
43
src/ClientApp/src/pages/Home/CallToActionSection.tsx
Normal file
43
src/ClientApp/src/pages/Home/CallToActionSection.tsx
Normal 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
|
||||
}
|
||||
100
src/ClientApp/src/pages/Home/FeaturedBlogsSection.tsx
Normal file
100
src/ClientApp/src/pages/Home/FeaturedBlogsSection.tsx
Normal 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
|
||||
}
|
||||
50
src/ClientApp/src/pages/Home/FeaturesSection.tsx
Normal file
50
src/ClientApp/src/pages/Home/FeaturesSection.tsx
Normal 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
|
||||
}
|
||||
47
src/ClientApp/src/pages/Home/TestimonialsSection.tsx
Normal file
47
src/ClientApp/src/pages/Home/TestimonialsSection.tsx
Normal 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
|
||||
}
|
||||
36
src/ClientApp/src/pages/Home/TitleSection.tsx
Normal file
36
src/ClientApp/src/pages/Home/TitleSection.tsx
Normal 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
|
||||
}
|
||||
@ -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)} · {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 {
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
|
||||
123
src/ClientApp/src/pages/Shop/Catalog/ShopItemsSection.tsx
Normal file
123
src/ClientApp/src/pages/Shop/Catalog/ShopItemsSection.tsx
Normal 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
|
||||
}
|
||||
30
src/ClientApp/src/pages/Shop/Catalog/TitleSection.tsx
Normal file
30
src/ClientApp/src/pages/Shop/Catalog/TitleSection.tsx
Normal 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
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
77
src/ClientApp/src/pages/Shop/Item/CommentsComponent.tsx
Normal file
77
src/ClientApp/src/pages/Shop/Item/CommentsComponent.tsx
Normal 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
|
||||
}
|
||||
@ -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>
|
||||
}
|
||||
@ -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} />
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
137
src/ClientApp/src/store/reducers/Comments.ts
Normal file
137
src/ClientApp/src/store/reducers/Comments.ts
Normal 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
|
||||
}
|
||||
@ -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} · 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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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...",
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
37
src/DomainObjects/Abstractions/Posts/L10n/PostItemL10n.cs
Normal file
37
src/DomainObjects/Abstractions/Posts/L10n/PostItemL10n.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
src/DomainObjects/Abstractions/Posts/PostItemBase.cs
Normal file
25
src/DomainObjects/Abstractions/Posts/PostItemBase.cs
Normal 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; }
|
||||
}
|
||||
13
src/DomainObjects/Documents/Content/ContentDocument.cs
Normal file
13
src/DomainObjects/Documents/Content/ContentDocument.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
41
src/DomainObjects/Documents/Content/L10n/ContentL10n.cs
Normal file
41
src/DomainObjects/Documents/Content/L10n/ContentL10n.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
using DomainObjects.Abstractions;
|
||||
using DomainObjects.Abstractions.Posts;
|
||||
|
||||
namespace DomainObjects.Documents.Posts;
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using DomainObjects.Abstractions;
|
||||
using DomainObjects.Abstractions.Posts;
|
||||
|
||||
namespace DomainObjects.Documents.Posts;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -7,6 +7,7 @@ using DomainObjects;
|
||||
|
||||
using Core.Enumerations;
|
||||
using Core.Abstractions.Models;
|
||||
using DomainObjects.Abstractions.Posts.L10n;
|
||||
|
||||
namespace WeatherForecast.Models.Blog.Requests {
|
||||
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user