From 2f3c452e114ca9f14cd33606ddd6fad6d3ca79fa Mon Sep 17 00:00:00 2001 From: "naveenkumar.kumanan" Date: Sat, 12 Jul 2025 11:59:19 +0530 Subject: [PATCH 01/15] feat: Add Istio sidecar resource annotations for cart, products, search, store-ui, and users deployments --- infra/k8s/apps/base/cart/deployment.yaml | 5 +++++ infra/k8s/apps/base/products/deployment.yaml | 5 +++++ infra/k8s/apps/base/search/deployment.yaml | 5 +++++ infra/k8s/apps/base/store-ui/deployment.yaml | 6 ++++++ infra/k8s/apps/base/users/deployment.yaml | 5 +++++ 5 files changed, 26 insertions(+) diff --git a/infra/k8s/apps/base/cart/deployment.yaml b/infra/k8s/apps/base/cart/deployment.yaml index f763a25..025ca7b 100644 --- a/infra/k8s/apps/base/cart/deployment.yaml +++ b/infra/k8s/apps/base/cart/deployment.yaml @@ -10,6 +10,11 @@ spec: metadata: labels: app: cart-deployment + annotations: + sidecar.istio.io/proxyCPU: "0" + sidecar.istio.io/proxyCPULimit: "0" + sidecar.istio.io/proxyMemory: "0" + sidecar.istio.io/proxyMemoryLimit: "0" spec: containers: - name: cart diff --git a/infra/k8s/apps/base/products/deployment.yaml b/infra/k8s/apps/base/products/deployment.yaml index c738770..3c95d1d 100644 --- a/infra/k8s/apps/base/products/deployment.yaml +++ b/infra/k8s/apps/base/products/deployment.yaml @@ -10,6 +10,11 @@ spec: metadata: labels: app: products-deployment + annotations: + sidecar.istio.io/proxyCPU: "0" + sidecar.istio.io/proxyCPULimit: "0" + sidecar.istio.io/proxyMemory: "0" + sidecar.istio.io/proxyMemoryLimit: "0" spec: containers: - name: products diff --git a/infra/k8s/apps/base/search/deployment.yaml b/infra/k8s/apps/base/search/deployment.yaml index 7f5c434..b816626 100644 --- a/infra/k8s/apps/base/search/deployment.yaml +++ b/infra/k8s/apps/base/search/deployment.yaml @@ -10,6 +10,11 @@ spec: metadata: labels: app: search-deployment + annotations: + sidecar.istio.io/proxyCPU: "0" + sidecar.istio.io/proxyCPULimit: "0" + sidecar.istio.io/proxyMemory: "0" + sidecar.istio.io/proxyMemoryLimit: "0" spec: containers: - name: search diff --git a/infra/k8s/apps/base/store-ui/deployment.yaml b/infra/k8s/apps/base/store-ui/deployment.yaml index 40a3800..b740f0e 100644 --- a/infra/k8s/apps/base/store-ui/deployment.yaml +++ b/infra/k8s/apps/base/store-ui/deployment.yaml @@ -2,6 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: store-ui-deployment + spec: selector: matchLabels: @@ -10,6 +11,11 @@ spec: metadata: labels: app: store-ui-deployment + annotations: + sidecar.istio.io/proxyCPU: "0" + sidecar.istio.io/proxyCPULimit: "0" + sidecar.istio.io/proxyMemory: "0" + sidecar.istio.io/proxyMemoryLimit: "0" spec: containers: - name: store-ui diff --git a/infra/k8s/apps/base/users/deployment.yaml b/infra/k8s/apps/base/users/deployment.yaml index 1a64298..2c10f0c 100644 --- a/infra/k8s/apps/base/users/deployment.yaml +++ b/infra/k8s/apps/base/users/deployment.yaml @@ -10,6 +10,11 @@ spec: metadata: labels: app: users-deployment + annotations: + sidecar.istio.io/proxyCPU: "0" + sidecar.istio.io/proxyCPULimit: "0" + sidecar.istio.io/proxyMemory: "0" + sidecar.istio.io/proxyMemoryLimit: "0" spec: containers: - name: users From 85089669b620bcf0661b07b487217e67af77eff8 Mon Sep 17 00:00:00 2001 From: "naveenkumar.kumanan" Date: Sun, 13 Jul 2025 12:59:07 +0530 Subject: [PATCH 02/15] feat: Refactor DevSpace configurations for cart, products, search, store-ui, and users services; add common scripts and Azure support --- cart-cna-microservice/devspace.yaml | 33 +++----- devspace-common.yaml | 79 +++++++++++++++++++ .../overlays/azure/cart/kustomization.yaml | 11 +++ .../apps/overlays/azure/kustomization.yaml | 10 +++ .../azure/products/kustomization.yaml | 11 +++ .../overlays/azure/search/kustomization.yaml | 11 +++ .../azure/store-ui/kustomization.yaml | 23 ++++++ .../overlays/azure/users/kustomization.yaml | 11 +++ .../overlays/local/cart/kustomization.yaml | 5 ++ .../local/products/kustomization.yaml | 6 ++ .../overlays/local/search/kustomization.yaml | 6 ++ .../local/store-ui/kustomization.yaml | 5 ++ .../overlays/local/users/kustomization.yaml | 6 ++ products-cna-microservice/devspace.yaml | 36 +++++---- search-cna-microservice/devspace.yaml | 33 +++----- store-ui/devspace.yaml | 32 +++----- users-cna-microservice/devspace.yaml | 22 +++--- 17 files changed, 244 insertions(+), 96 deletions(-) create mode 100644 devspace-common.yaml create mode 100644 infra/k8s/apps/overlays/azure/cart/kustomization.yaml create mode 100644 infra/k8s/apps/overlays/azure/kustomization.yaml create mode 100644 infra/k8s/apps/overlays/azure/products/kustomization.yaml create mode 100644 infra/k8s/apps/overlays/azure/search/kustomization.yaml create mode 100644 infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml create mode 100644 infra/k8s/apps/overlays/azure/users/kustomization.yaml diff --git a/cart-cna-microservice/devspace.yaml b/cart-cna-microservice/devspace.yaml index 06f332b..a68811e 100644 --- a/cart-cna-microservice/devspace.yaml +++ b/cart-cna-microservice/devspace.yaml @@ -1,19 +1,16 @@ version: v2beta1 name: cart -images: - cart: - image: image.registry.local:5001/cart - dockerfile: ./Dockerfile - tags: - - latest +imports: + - path: "../devspace-common.yaml" -deployments: - cart: - kubectl: - manifests: - - /home/naveenkumar.kumanan/Naveen_personal/e-commerce-microservices-sample/infra/k8s/apps/overlays/local/cart/ - kustomize: true +vars: + SERVICE_NAME: + default: "cart" + K8S_MANIFEST_PATH_LOCAL: + default: "../infra/k8s/apps/overlays/local/cart/" + K8S_MANIFEST_PATH_AZURE: + default: "../infra/k8s/apps/overlays/azure/cart/" dev: cart: @@ -47,14 +44,4 @@ dev: - name: APP_ENVIRONMENT value: "development" - name: LOG_LEVEL_ROOT - value: "INFO" - -commands: - start: - command: devspace enter cart -- ./gradlew bootRun - build: - command: devspace enter cart -- ./gradlew build - test: - command: devspace enter cart -- ./gradlew test - clean: - command: devspace enter cart -- ./gradlew clean \ No newline at end of file + value: "INFO" \ No newline at end of file diff --git a/devspace-common.yaml b/devspace-common.yaml new file mode 100644 index 0000000..e260220 --- /dev/null +++ b/devspace-common.yaml @@ -0,0 +1,79 @@ +version: v2beta1 +name: common-scripts + +vars: + REGISTRY: + default: "image.registry.local:5001" + AZURE_REGISTRY: + default: "kubecondemo.azurecr.io" + IMAGE_TAG: + default: "latest" + +# Generic images template - will be inherited by services +images: + app: + image: ${REGISTRY}/${SERVICE_NAME} + dockerfile: ./Dockerfile + tags: + - ${IMAGE_TAG} + +# Generic deployments template - will be inherited by services +deployments: + app: + kubectl: + manifests: + - ${K8S_MANIFEST_PATH_LOCAL} + kustomize: true + +# Azure ACR profile for Kubernetes +profiles: + - name: azure + description: "Azure ACR for Kubernetes deployment" + patches: + - op: replace + path: vars.REGISTRY + value: ${AZURE_REGISTRY} + - op: replace + path: deployments.app.kubectl.manifests[0] + value: ${K8S_MANIFEST_PATH_AZURE} + activation: + - vars: + DEVSPACE_PROFILE: "azure" + + +# Standardized commands for consistency across all microservices +commands: + # Development commands + dev: + command: devspace dev --skip-build --skip-deploy + description: "Start development mode with hot reload" + + # Core deployment commands + deploy: + command: devspace deploy + description: "Deploy to local environment" + + build: + command: devspace build + description: "Build image for local registry" + + purge: + command: devspace purge + description: "Remove deployment from cluster" + + # Azure profile commands + deploy-azure: + command: devspace deploy --profile azure + description: "Deploy to Azure ACR environment" + + build-azure: + command: devspace build --profile azure + description: "Build image for Azure ACR" + + dev-azure: + command: devspace dev --skip-build --skip-deploy --profile azure + description: "Start development mode with Azure profile" + + purge-azure: + command: devspace purge --profile azure + description: "Remove Azure deployment from cluster" \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/cart/kustomization.yaml b/infra/k8s/apps/overlays/azure/cart/kustomization.yaml new file mode 100644 index 0000000..cf2817e --- /dev/null +++ b/infra/k8s/apps/overlays/azure/cart/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: e-commerce + +resources: +- ../../../base/cart + +images: +- name: cart:latest + newName: kubecondemo.azurecr.io/cart + newTag: latest \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/kustomization.yaml b/infra/k8s/apps/overlays/azure/kustomization.yaml new file mode 100644 index 0000000..b841ed1 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: e-commerce + +resources: +- store-ui +- cart +- products +- search +- users \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/products/kustomization.yaml b/infra/k8s/apps/overlays/azure/products/kustomization.yaml new file mode 100644 index 0000000..831997b --- /dev/null +++ b/infra/k8s/apps/overlays/azure/products/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: e-commerce + +resources: +- ../../../base/products + +images: +- name: products:latest + newName: kubecondemo.azurecr.io/products + newTag: latest \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/search/kustomization.yaml b/infra/k8s/apps/overlays/azure/search/kustomization.yaml new file mode 100644 index 0000000..d8ca7f4 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/search/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: e-commerce + +resources: +- ../../../base/search + +images: +- name: search:latest + newName: kubecondemo.azurecr.io/search + newTag: latest \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml b/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml new file mode 100644 index 0000000..60f558d --- /dev/null +++ b/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml @@ -0,0 +1,23 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: e-commerce + +resources: +- ../../../base/store-ui + +images: +- name: store-ui:latest + newName: kubecondemo.azurecr.io/store-ui + newTag: latest + +configMapGenerator: +- behavior: create + literals: + # Use Kubernetes service URLs for in-cluster communication + - REACT_APP_PRODUCTS_URL_BASE=http://products-service:5000/ + - REACT_APP_CART_URL_BASE=http://cart-service:7000/ + - REACT_APP_SEARCH_URL_BASE=http://search-service:4000/ + - REACT_APP_USERS_URL_BASE=http://users-service:9090/ + name: store-ui-configmap +generatorOptions: + disableNameSuffixHash: true \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/users/kustomization.yaml b/infra/k8s/apps/overlays/azure/users/kustomization.yaml new file mode 100644 index 0000000..4920507 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/users/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: e-commerce + +resources: +- ../../../base/users + +images: +- name: users:latest + newName: kubecondemo.azurecr.io/users + newTag: latest \ No newline at end of file diff --git a/infra/k8s/apps/overlays/local/cart/kustomization.yaml b/infra/k8s/apps/overlays/local/cart/kustomization.yaml index b56bc55..af33cf8 100644 --- a/infra/k8s/apps/overlays/local/cart/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/cart/kustomization.yaml @@ -5,6 +5,11 @@ resources: - ../../../base/cart - service-nodeport.yaml +images: +- name: cart:latest + newName: image.registry.local:5001/cart + newTag: latest + configMapGenerator: - behavior: create literals: diff --git a/infra/k8s/apps/overlays/local/products/kustomization.yaml b/infra/k8s/apps/overlays/local/products/kustomization.yaml index f59297c..01424ab 100644 --- a/infra/k8s/apps/overlays/local/products/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/products/kustomization.yaml @@ -4,6 +4,12 @@ namespace: e-commerce resources: - ../../../base/products - service-nodeport.yaml + +images: +- name: products:latest + newName: image.registry.local:5001/products + newTag: latest + configMapGenerator: - behavior: create literals: diff --git a/infra/k8s/apps/overlays/local/search/kustomization.yaml b/infra/k8s/apps/overlays/local/search/kustomization.yaml index 38f0ff3..7037d97 100644 --- a/infra/k8s/apps/overlays/local/search/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/search/kustomization.yaml @@ -4,6 +4,12 @@ namespace: e-commerce resources: - ../../../base/search - service-nodeport.yaml + +images: +- name: search:latest + newName: image.registry.local:5001/search + newTag: latest + configMapGenerator: - behavior: create literals: diff --git a/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml b/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml index d459b3d..a5f609c 100644 --- a/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml @@ -6,6 +6,11 @@ resources: - ../../../base/store-ui - service-nodeport.yaml +images: +- name: store-ui:latest + newName: image.registry.local:5001/store-ui + newTag: latest + configMapGenerator: - behavior: create literals: diff --git a/infra/k8s/apps/overlays/local/users/kustomization.yaml b/infra/k8s/apps/overlays/local/users/kustomization.yaml index 9d95475..3e321b4 100644 --- a/infra/k8s/apps/overlays/local/users/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/users/kustomization.yaml @@ -4,6 +4,12 @@ namespace: e-commerce resources: - ../../../base/users - service-nodeport.yaml + +images: +- name: users:latest + newName: image.registry.local:5001/users + newTag: latest + configMapGenerator: - behavior: create literals: diff --git a/products-cna-microservice/devspace.yaml b/products-cna-microservice/devspace.yaml index 2f221c4..1b473e2 100644 --- a/products-cna-microservice/devspace.yaml +++ b/products-cna-microservice/devspace.yaml @@ -1,19 +1,16 @@ version: v2beta1 name: products -images: - products: - image: image.registry.local:5001/products - dockerfile: ./Dockerfile - tags: - - latest +imports: + - path: "../devspace-common.yaml" -deployments: - products: - kubectl: - manifests: - - /home/naveenkumar.kumanan/Naveen_personal/e-commerce-microservices-sample/infra/k8s/apps/overlays/local/products/ - kustomize: true +vars: + SERVICE_NAME: + default: "products" + K8S_MANIFEST_PATH_LOCAL: + default: "../infra/k8s/apps/overlays/local/products/" + K8S_MANIFEST_PATH_AZURE: + default: "../infra/k8s/apps/overlays/azure/products/" dev: products: @@ -21,10 +18,19 @@ dev: app: products-deployment container: products devImage: ghcr.io/loft-sh/devspace-containers/javascript:18-alpine - # Sync files between the local filesystem and the development container sync: - - path: ./ - uploadExcludeFile: .dockerignore + - path: ./:/app + excludePaths: + - .dockerignore + - .devspace/ + - node_modules/ + - build/ + - .git/ + - "*.log" + - package-lock.json + - yarn.lock + initialSync: preferLocal + printLogs: true terminal: command: ./devspace_start.sh ssh: diff --git a/search-cna-microservice/devspace.yaml b/search-cna-microservice/devspace.yaml index 0947413..7b0fe1f 100644 --- a/search-cna-microservice/devspace.yaml +++ b/search-cna-microservice/devspace.yaml @@ -1,19 +1,16 @@ version: v2beta1 name: search -images: - search: - image: image.registry.local:5001/search - dockerfile: ./Dockerfile - tags: - - latest +imports: + - path: "../devspace-common.yaml" -deployments: - search: - kubectl: - manifests: - - /home/naveenkumar.kumanan/Naveen_personal/e-commerce-microservices-sample/infra/k8s/apps/overlays/local/search/ - kustomize: true +vars: + SERVICE_NAME: + default: "search" + K8S_MANIFEST_PATH_LOCAL: + default: "../infra/k8s/apps/overlays/local/search/" + K8S_MANIFEST_PATH_AZURE: + default: "../infra/k8s/apps/overlays/azure/search/" dev: search: @@ -34,20 +31,8 @@ dev: command: ./devspace_start.sh ports: - port: "4000" - open: - - url: http://localhost:4000 env: - name: NODE_ENV value: "development" - name: ELASTIC_URL value: "http://localhost:9200/" - -commands: - start: - command: devspace enter search -- npm start - build: - command: devspace enter search -- npm run build - test: - command: devspace enter search -- npm test - install: - command: devspace enter search -- npm install diff --git a/store-ui/devspace.yaml b/store-ui/devspace.yaml index 1593d80..0f94da8 100644 --- a/store-ui/devspace.yaml +++ b/store-ui/devspace.yaml @@ -1,17 +1,16 @@ version: v2beta1 name: store-ui -images: - store-ui: - image: image.registry.local:5001/store-ui - dockerfile: ./Dockerfile +imports: + - path: "../devspace-common.yaml" -deployments: - store-ui: - kubectl: - manifests: - - /home/naveenkumar.kumanan/Naveen_personal/e-commerce-microservices-sample/infra/k8s/apps/overlays/local/store-ui/ - kustomize: true +vars: + SERVICE_NAME: + default: "store-ui" + K8S_MANIFEST_PATH_LOCAL: + default: "../infra/k8s/apps/overlays/local/store-ui/" + K8S_MANIFEST_PATH_AZURE: + default: "../infra/k8s/apps/overlays/azure/store-ui/" dev: store-ui: @@ -47,15 +46,4 @@ dev: - name: REACT_APP_SEARCH_URL_BASE value: "http://localhost:8082/" - name: REACT_APP_USERS_URL_BASE - value: "http://localhost:8083/" - - -commands: - start: - command: devspace enter store-ui -- npm start - build: - command: devspace enter store-ui -- npm run build - test: - command: devspace enter store-ui -- npm test - install: - command: devspace enter store-ui -- npm install + value: "http://localhost:8083/" \ No newline at end of file diff --git a/users-cna-microservice/devspace.yaml b/users-cna-microservice/devspace.yaml index 4822e15..7fa29df 100755 --- a/users-cna-microservice/devspace.yaml +++ b/users-cna-microservice/devspace.yaml @@ -1,19 +1,16 @@ version: v2beta1 name: users -images: - users: - image: image.registry.local:5001/users - dockerfile: ./Dockerfile - tags: - - latest +imports: + - path: "../devspace-common.yaml" -deployments: - users: - kubectl: - manifests: - - /home/naveenkumar.kumanan/Naveen_personal/e-commerce-microservices-sample/infra/k8s/apps/overlays/local/users/ - kustomize: true +vars: + SERVICE_NAME: + default: "users" + K8S_MANIFEST_PATH_LOCAL: + default: "../infra/k8s/apps/overlays/local/users/" + K8S_MANIFEST_PATH_AZURE: + default: "../infra/k8s/apps/overlays/azure/users/" dev: users: @@ -29,6 +26,7 @@ dev: - "*.pyc" - .pytest_cache/ - .git/ + - "*.log" initialSync: preferLocal printLogs: true terminal: From dd42905b218f963490696a472c9ae45297eeafe8 Mon Sep 17 00:00:00 2001 From: "naveenkumar.kumanan" Date: Sun, 13 Jul 2025 13:05:43 +0530 Subject: [PATCH 03/15] feat: Add Istio Gateway and VirtualService configurations for store-ui --- .../azure/store-ui/kustomization.yaml | 2 ++ .../azure/store-ui/store-ui-gateway.yaml | 15 +++++++++++++++ .../overlays/azure/store-ui/store-ui-vs.yaml | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 infra/k8s/apps/overlays/azure/store-ui/store-ui-gateway.yaml create mode 100644 infra/k8s/apps/overlays/azure/store-ui/store-ui-vs.yaml diff --git a/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml b/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml index 60f558d..bacb7b0 100644 --- a/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml @@ -4,6 +4,8 @@ namespace: e-commerce resources: - ../../../base/store-ui +- store-ui-gateway.yaml +- store-ui-vs.yaml images: - name: store-ui:latest diff --git a/infra/k8s/apps/overlays/azure/store-ui/store-ui-gateway.yaml b/infra/k8s/apps/overlays/azure/store-ui/store-ui-gateway.yaml new file mode 100644 index 0000000..1db6c9d --- /dev/null +++ b/infra/k8s/apps/overlays/azure/store-ui/store-ui-gateway.yaml @@ -0,0 +1,15 @@ +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: store-ui-gateway + namespace: aks-istio-ingress +spec: + selector: + istio: aks-istio-ingressgateway-external + servers: + - port: + number: 80 + name: http + protocol: HTTP + hosts: + - "*" \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/store-ui/store-ui-vs.yaml b/infra/k8s/apps/overlays/azure/store-ui/store-ui-vs.yaml new file mode 100644 index 0000000..5851df5 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/store-ui/store-ui-vs.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: store-ui + namespace: e-commerce +spec: + hosts: + - "*" + gateways: + - aks-istio-ingress/store-ui-gateway # namespace/name + http: + - match: # everything that hits "/" + - uri: + prefix: / + route: + - destination: + host: store-ui-service.e-commerce.svc.cluster.local + port: + number: 80 # servicePort in the Service object \ No newline at end of file From 9a8c983815f4163b52383119115042b0392c448d Mon Sep 17 00:00:00 2001 From: "naveenkumar.kumanan" Date: Sun, 13 Jul 2025 13:23:21 +0530 Subject: [PATCH 04/15] feat: Add VirtualService configurations for cart, products, search, and users; update kustomization files --- .../k8s/apps/overlays/azure/cart/cart-vs.yaml | 21 +++++++++++++++++++ .../overlays/azure/cart/kustomization.yaml | 1 + .../azure/products/kustomization.yaml | 1 + .../overlays/azure/products/products-vs.yaml | 21 +++++++++++++++++++ .../overlays/azure/search/kustomization.yaml | 1 + .../apps/overlays/azure/search/search-vs.yaml | 21 +++++++++++++++++++ .../azure/store-ui/kustomization.yaml | 10 ++++----- .../overlays/azure/users/kustomization.yaml | 1 + .../apps/overlays/azure/users/users-vs.yaml | 21 +++++++++++++++++++ 9 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 infra/k8s/apps/overlays/azure/cart/cart-vs.yaml create mode 100644 infra/k8s/apps/overlays/azure/products/products-vs.yaml create mode 100644 infra/k8s/apps/overlays/azure/search/search-vs.yaml create mode 100644 infra/k8s/apps/overlays/azure/users/users-vs.yaml diff --git a/infra/k8s/apps/overlays/azure/cart/cart-vs.yaml b/infra/k8s/apps/overlays/azure/cart/cart-vs.yaml new file mode 100644 index 0000000..b59d8b6 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/cart/cart-vs.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: cart + namespace: e-commerce +spec: + hosts: + - "*" + gateways: + - aks-istio-ingress/store-ui-gateway + http: + - match: + - uri: + prefix: /api/cart + rewrite: + uri: / + route: + - destination: + host: cart-service.e-commerce.svc.cluster.local + port: + number: 7000 \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/cart/kustomization.yaml b/infra/k8s/apps/overlays/azure/cart/kustomization.yaml index cf2817e..99f88fb 100644 --- a/infra/k8s/apps/overlays/azure/cart/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/cart/kustomization.yaml @@ -4,6 +4,7 @@ namespace: e-commerce resources: - ../../../base/cart +- cart-vs.yaml images: - name: cart:latest diff --git a/infra/k8s/apps/overlays/azure/products/kustomization.yaml b/infra/k8s/apps/overlays/azure/products/kustomization.yaml index 831997b..bcb7940 100644 --- a/infra/k8s/apps/overlays/azure/products/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/products/kustomization.yaml @@ -4,6 +4,7 @@ namespace: e-commerce resources: - ../../../base/products +- products-vs.yaml images: - name: products:latest diff --git a/infra/k8s/apps/overlays/azure/products/products-vs.yaml b/infra/k8s/apps/overlays/azure/products/products-vs.yaml new file mode 100644 index 0000000..e3a9c28 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/products/products-vs.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: products + namespace: e-commerce +spec: + hosts: + - "*" + gateways: + - aks-istio-ingress/store-ui-gateway + http: + - match: + - uri: + prefix: /api/products + rewrite: + uri: / + route: + - destination: + host: products-service.e-commerce.svc.cluster.local + port: + number: 5000 \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/search/kustomization.yaml b/infra/k8s/apps/overlays/azure/search/kustomization.yaml index d8ca7f4..08ef30e 100644 --- a/infra/k8s/apps/overlays/azure/search/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/search/kustomization.yaml @@ -4,6 +4,7 @@ namespace: e-commerce resources: - ../../../base/search +- search-vs.yaml images: - name: search:latest diff --git a/infra/k8s/apps/overlays/azure/search/search-vs.yaml b/infra/k8s/apps/overlays/azure/search/search-vs.yaml new file mode 100644 index 0000000..5b98cdc --- /dev/null +++ b/infra/k8s/apps/overlays/azure/search/search-vs.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: search + namespace: e-commerce +spec: + hosts: + - "*" + gateways: + - aks-istio-ingress/store-ui-gateway + http: + - match: + - uri: + prefix: /api/search + rewrite: + uri: / + route: + - destination: + host: search-service.e-commerce.svc.cluster.local + port: + number: 4000 \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml b/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml index bacb7b0..6c0fbb2 100644 --- a/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml @@ -15,11 +15,11 @@ images: configMapGenerator: - behavior: create literals: - # Use Kubernetes service URLs for in-cluster communication - - REACT_APP_PRODUCTS_URL_BASE=http://products-service:5000/ - - REACT_APP_CART_URL_BASE=http://cart-service:7000/ - - REACT_APP_SEARCH_URL_BASE=http://search-service:4000/ - - REACT_APP_USERS_URL_BASE=http://users-service:9090/ + # Use the Istio Gateway URL for browser-based API calls + - REACT_APP_PRODUCTS_URL_BASE=/api/products/ + - REACT_APP_CART_URL_BASE=/api/cart/ + - REACT_APP_SEARCH_URL_BASE=/api/search/ + - REACT_APP_USERS_URL_BASE=/api/users/ name: store-ui-configmap generatorOptions: disableNameSuffixHash: true \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/users/kustomization.yaml b/infra/k8s/apps/overlays/azure/users/kustomization.yaml index 4920507..f4857c6 100644 --- a/infra/k8s/apps/overlays/azure/users/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/users/kustomization.yaml @@ -4,6 +4,7 @@ namespace: e-commerce resources: - ../../../base/users +- users-vs.yaml images: - name: users:latest diff --git a/infra/k8s/apps/overlays/azure/users/users-vs.yaml b/infra/k8s/apps/overlays/azure/users/users-vs.yaml new file mode 100644 index 0000000..b266b22 --- /dev/null +++ b/infra/k8s/apps/overlays/azure/users/users-vs.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: users + namespace: e-commerce +spec: + hosts: + - "*" + gateways: + - aks-istio-ingress/store-ui-gateway + http: + - match: + - uri: + prefix: /api/users + rewrite: + uri: / + route: + - destination: + host: users-service.e-commerce.svc.cluster.local + port: + number: 9090 \ No newline at end of file From f6b0ec71dce2f2407eaab518d36e6990e80422db Mon Sep 17 00:00:00 2001 From: "naveenkumar.kumanan" Date: Sun, 13 Jul 2025 15:16:14 +0530 Subject: [PATCH 05/15] Refactor Kubernetes manifest paths to use K8S_DIR variable for consistency - Updated devspace.yaml files for cart, products, search, store-ui, and users microservices to use ${K8S_DIR} for manifest paths. - Added NAMESPACE and K8S_DIR variables to devspace-common.yaml for dynamic namespace handling. - Introduced reusable pipelines in devspace-common.yaml for preparing and applying manifests with namespace replacement. - Modified kustomization.yaml files across various services to replace hardcoded namespace with NAMESPACE_PLACEHOLDER. - Created deploy-with-namespace.sh and install.sh scripts for user-specific deployments and installations. - Enhanced uninstall.sh script to handle user-specific namespace removal and shared services cleanup. --- cart-cna-microservice/devspace.yaml | 4 +- devspace-common.yaml | 168 ++++++++++++++---- infra/k8s/apps/base/cart/kustomization.yaml | 2 +- .../k8s/apps/base/products/kustomization.yaml | 2 +- infra/k8s/apps/base/search/kustomization.yaml | 2 +- .../k8s/apps/base/store-ui/kustomization.yaml | 2 +- infra/k8s/apps/base/users/kustomization.yaml | 2 +- .../k8s/apps/overlays/azure/cart/cart-vs.yaml | 4 +- .../overlays/azure/cart/kustomization.yaml | 2 +- .../apps/overlays/azure/kustomization.yaml | 2 +- .../azure/products/kustomization.yaml | 2 +- .../overlays/azure/products/products-vs.yaml | 4 +- .../overlays/azure/search/kustomization.yaml | 2 +- .../apps/overlays/azure/search/search-vs.yaml | 4 +- .../azure/store-ui/kustomization.yaml | 2 +- .../overlays/azure/store-ui/store-ui-vs.yaml | 6 +- .../overlays/azure/users/kustomization.yaml | 2 +- .../apps/overlays/azure/users/users-vs.yaml | 4 +- .../overlays/local/cart/kustomization.yaml | 2 +- .../local/products/kustomization.yaml | 4 +- .../overlays/local/search/kustomization.yaml | 2 +- .../local/store-ui/kustomization.yaml | 2 +- .../overlays/local/users/kustomization.yaml | 4 +- infra/k8s/deploy-with-namespace.sh | 63 +++++++ infra/k8s/install.sh | 147 +++++++++++++++ infra/k8s/uninstall.sh | 143 +++++++++++++++ products-cna-microservice/devspace.yaml | 4 +- search-cna-microservice/devspace.yaml | 4 +- store-ui/devspace.yaml | 4 +- users-cna-microservice/devspace.yaml | 4 +- 30 files changed, 528 insertions(+), 71 deletions(-) create mode 100644 infra/k8s/deploy-with-namespace.sh create mode 100755 infra/k8s/install.sh create mode 100755 infra/k8s/uninstall.sh diff --git a/cart-cna-microservice/devspace.yaml b/cart-cna-microservice/devspace.yaml index a68811e..ff58dd7 100644 --- a/cart-cna-microservice/devspace.yaml +++ b/cart-cna-microservice/devspace.yaml @@ -8,9 +8,9 @@ vars: SERVICE_NAME: default: "cart" K8S_MANIFEST_PATH_LOCAL: - default: "../infra/k8s/apps/overlays/local/cart/" + default: "${K8S_DIR}/infra/k8s/apps/overlays/local/cart/" K8S_MANIFEST_PATH_AZURE: - default: "../infra/k8s/apps/overlays/azure/cart/" + default: "${K8S_DIR}/infra/k8s/apps/overlays/azure/cart/" dev: cart: diff --git a/devspace-common.yaml b/devspace-common.yaml index e260220..5c27c43 100644 --- a/devspace-common.yaml +++ b/devspace-common.yaml @@ -8,6 +8,12 @@ vars: default: "kubecondemo.azurecr.io" IMAGE_TAG: default: "latest" + # Use sanitized username as default namespace + NAMESPACE: + command: "echo $USER | tr '.' '-'" + # Base directory for Kubernetes manifests + K8S_DIR: + default: ".." # Generic images template - will be inherited by services images: @@ -17,63 +23,161 @@ images: tags: - ${IMAGE_TAG} -# Generic deployments template - will be inherited by services -deployments: - app: - kubectl: - manifests: - - ${K8S_MANIFEST_PATH_LOCAL} - kustomize: true - -# Azure ACR profile for Kubernetes -profiles: - - name: azure - description: "Azure ACR for Kubernetes deployment" - patches: - - op: replace - path: vars.REGISTRY - value: ${AZURE_REGISTRY} - - op: replace - path: deployments.app.kubectl.manifests[0] - value: ${K8S_MANIFEST_PATH_AZURE} - activation: - - vars: - DEVSPACE_PROFILE: "azure" - +# Define reusable pipelines to reduce duplication +pipelines: + # Common pipeline to prepare manifests with namespace replacement + prepare-manifests: + run: | + # Create a temporary directory for the customized yaml + TEMP_DIR=$(mktemp -d) + echo "Creating temporary files in $TEMP_DIR" + echo "export TEMP_DIR=$TEMP_DIR" > ~/.devspace/vars/temp_dir.env + + # Get the base directory + source ~/.devspace/vars/env_type.env + source ~/.devspace/vars/k8s_path.env + MANIFEST_PATH="${K8S_PATH}" + SERVICE_DIR=$(basename "${MANIFEST_PATH}") + BASE_DIR=$(dirname "${MANIFEST_PATH}") + BASE_DIR=$(dirname "$BASE_DIR") + BASE_DIR=$(dirname "$BASE_DIR") + + # Copy the necessary files to the temp directory + mkdir -p $TEMP_DIR/base $TEMP_DIR/overlays/$ENV_TYPE + cp -r ${BASE_DIR}/base/* $TEMP_DIR/base/ + cp -r ${BASE_DIR}/overlays/$ENV_TYPE/* $TEMP_DIR/overlays/$ENV_TYPE/ + + # Save paths for later steps + echo "export SERVICE_DIR=$SERVICE_DIR" >> ~/.devspace/vars/temp_dir.env + echo "export BASE_DIR=$BASE_DIR" >> ~/.devspace/vars/temp_dir.env + + # Replace in all yaml files + find $TEMP_DIR -type f -name "*.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/${NAMESPACE}/g" {} \; + + # Create the namespace if it doesn't exist + kubectl create namespace ${NAMESPACE} --dry-run=client -o yaml | kubectl apply -f - + + echo "Prepared Kubernetes manifests in $TEMP_DIR with namespace: ${NAMESPACE}" + + # Apply the prepared manifests + apply-manifests: + run: | + source ~/.devspace/vars/temp_dir.env + source ~/.devspace/vars/env_type.env + kubectl apply -k $TEMP_DIR/overlays/$ENV_TYPE/$SERVICE_DIR + + # Handle Istio Gateway for Azure environment + if [[ "$ENV_TYPE" == "azure" ]]; then + # Check if the Istio Gateway exists in the aks-istio-ingress namespace + GATEWAY_EXISTS=$(kubectl get gateway -n aks-istio-ingress store-ui-gateway -o name --ignore-not-found) + + if [ -z "$GATEWAY_EXISTS" ]; then + echo "Creating Istio Gateway in aks-istio-ingress namespace" + # Apply the Gateway configuration separately since it's in a different namespace + GATEWAY_FILE="${BASE_DIR}/overlays/azure/store-ui/store-ui-gateway.yaml" + if [ -f "$GATEWAY_FILE" ]; then + kubectl apply -f "$GATEWAY_FILE" + fi + else + echo "Istio Gateway already exists in aks-istio-ingress namespace" + fi + fi + + # Delete the prepared manifests + delete-manifests: + run: | + source ~/.devspace/vars/temp_dir.env + source ~/.devspace/vars/env_type.env + kubectl delete -k $TEMP_DIR/overlays/$ENV_TYPE/$SERVICE_DIR --ignore-not-found + + # Clean up the temporary directory + cleanup: + run: | + source ~/.devspace/vars/temp_dir.env + rm -rf $TEMP_DIR + echo "Cleaned up temporary directory" # Standardized commands for consistency across all microservices commands: # Development commands dev: - command: devspace dev --skip-build --skip-deploy + command: devspace use namespace ${NAMESPACE} && devspace dev --skip-build --skip-deploy description: "Start development mode with hot reload" # Core deployment commands deploy: - command: devspace deploy - description: "Deploy to local environment" + command: | + mkdir -p ~/.devspace/vars + # Set environment variables for pipelines + echo "export ENV_TYPE=local" > ~/.devspace/vars/env_type.env + echo "export K8S_PATH=${K8S_MANIFEST_PATH_LOCAL}" > ~/.devspace/vars/k8s_path.env + + # Run pipelines + devspace run-pipeline prepare-manifests + devspace run-pipeline apply-manifests + devspace run-pipeline cleanup + description: "Deploy to local environment with dynamic namespace" build: command: devspace build description: "Build image for local registry" purge: - command: devspace purge + command: | + mkdir -p ~/.devspace/vars + # Set environment variables for pipelines + echo "export ENV_TYPE=local" > ~/.devspace/vars/env_type.env + echo "export K8S_PATH=${K8S_MANIFEST_PATH_LOCAL}" > ~/.devspace/vars/k8s_path.env + + # Run pipelines + devspace run-pipeline prepare-manifests + devspace run-pipeline delete-manifests + devspace run-pipeline cleanup description: "Remove deployment from cluster" # Azure profile commands deploy-azure: - command: devspace deploy --profile azure - description: "Deploy to Azure ACR environment" + command: | + mkdir -p ~/.devspace/vars + # Set environment variables for pipelines + echo "export ENV_TYPE=azure" > ~/.devspace/vars/env_type.env + echo "export K8S_PATH=${K8S_MANIFEST_PATH_AZURE}" > ~/.devspace/vars/k8s_path.env + + # Run pipelines + devspace run-pipeline prepare-manifests + devspace run-pipeline apply-manifests + devspace run-pipeline cleanup + description: "Deploy to Azure with dynamic namespace" build-azure: command: devspace build --profile azure description: "Build image for Azure ACR" dev-azure: - command: devspace dev --skip-build --skip-deploy --profile azure + command: devspace use namespace ${NAMESPACE} && devspace dev --skip-build --skip-deploy --profile azure description: "Start development mode with Azure profile" purge-azure: - command: devspace purge --profile azure - description: "Remove Azure deployment from cluster" \ No newline at end of file + command: | + mkdir -p ~/.devspace/vars + # Set environment variables for pipelines + echo "export ENV_TYPE=azure" > ~/.devspace/vars/env_type.env + echo "export K8S_PATH=${K8S_MANIFEST_PATH_AZURE}" > ~/.devspace/vars/k8s_path.env + + # Run pipelines + devspace run-pipeline prepare-manifests + devspace run-pipeline delete-manifests + devspace run-pipeline cleanup + description: "Remove Azure deployment from cluster" + +# Azure ACR profile for Kubernetes +profiles: + - name: azure + description: "Azure ACR for Kubernetes deployment" + patches: + - op: replace + path: vars.REGISTRY + value: ${AZURE_REGISTRY} + activation: + - vars: + DEVSPACE_PROFILE: "azure" \ No newline at end of file diff --git a/infra/k8s/apps/base/cart/kustomization.yaml b/infra/k8s/apps/base/cart/kustomization.yaml index 6fdb37e..b573cdc 100644 --- a/infra/k8s/apps/base/cart/kustomization.yaml +++ b/infra/k8s/apps/base/cart/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - service.yaml - deployment.yaml \ No newline at end of file diff --git a/infra/k8s/apps/base/products/kustomization.yaml b/infra/k8s/apps/base/products/kustomization.yaml index 6fdb37e..b573cdc 100644 --- a/infra/k8s/apps/base/products/kustomization.yaml +++ b/infra/k8s/apps/base/products/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - service.yaml - deployment.yaml \ No newline at end of file diff --git a/infra/k8s/apps/base/search/kustomization.yaml b/infra/k8s/apps/base/search/kustomization.yaml index 6fdb37e..b573cdc 100644 --- a/infra/k8s/apps/base/search/kustomization.yaml +++ b/infra/k8s/apps/base/search/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - service.yaml - deployment.yaml \ No newline at end of file diff --git a/infra/k8s/apps/base/store-ui/kustomization.yaml b/infra/k8s/apps/base/store-ui/kustomization.yaml index 6fdb37e..b573cdc 100644 --- a/infra/k8s/apps/base/store-ui/kustomization.yaml +++ b/infra/k8s/apps/base/store-ui/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - service.yaml - deployment.yaml \ No newline at end of file diff --git a/infra/k8s/apps/base/users/kustomization.yaml b/infra/k8s/apps/base/users/kustomization.yaml index 6fdb37e..b573cdc 100644 --- a/infra/k8s/apps/base/users/kustomization.yaml +++ b/infra/k8s/apps/base/users/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - service.yaml - deployment.yaml \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/cart/cart-vs.yaml b/infra/k8s/apps/overlays/azure/cart/cart-vs.yaml index b59d8b6..0d8c483 100644 --- a/infra/k8s/apps/overlays/azure/cart/cart-vs.yaml +++ b/infra/k8s/apps/overlays/azure/cart/cart-vs.yaml @@ -2,7 +2,7 @@ apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: cart - namespace: e-commerce + namespace: NAMESPACE_PLACEHOLDER spec: hosts: - "*" @@ -16,6 +16,6 @@ spec: uri: / route: - destination: - host: cart-service.e-commerce.svc.cluster.local + host: cart-service.NAMESPACE_PLACEHOLDER.svc.cluster.local port: number: 7000 \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/cart/kustomization.yaml b/infra/k8s/apps/overlays/azure/cart/kustomization.yaml index 99f88fb..f940e3e 100644 --- a/infra/k8s/apps/overlays/azure/cart/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/cart/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/cart diff --git a/infra/k8s/apps/overlays/azure/kustomization.yaml b/infra/k8s/apps/overlays/azure/kustomization.yaml index b841ed1..846895f 100644 --- a/infra/k8s/apps/overlays/azure/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - store-ui diff --git a/infra/k8s/apps/overlays/azure/products/kustomization.yaml b/infra/k8s/apps/overlays/azure/products/kustomization.yaml index bcb7940..e577302 100644 --- a/infra/k8s/apps/overlays/azure/products/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/products/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/products diff --git a/infra/k8s/apps/overlays/azure/products/products-vs.yaml b/infra/k8s/apps/overlays/azure/products/products-vs.yaml index e3a9c28..b2d745c 100644 --- a/infra/k8s/apps/overlays/azure/products/products-vs.yaml +++ b/infra/k8s/apps/overlays/azure/products/products-vs.yaml @@ -2,7 +2,7 @@ apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: products - namespace: e-commerce + namespace: NAMESPACE_PLACEHOLDER spec: hosts: - "*" @@ -16,6 +16,6 @@ spec: uri: / route: - destination: - host: products-service.e-commerce.svc.cluster.local + host: products-service.NAMESPACE_PLACEHOLDER.svc.cluster.local port: number: 5000 \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/search/kustomization.yaml b/infra/k8s/apps/overlays/azure/search/kustomization.yaml index 08ef30e..378aab2 100644 --- a/infra/k8s/apps/overlays/azure/search/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/search/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/search diff --git a/infra/k8s/apps/overlays/azure/search/search-vs.yaml b/infra/k8s/apps/overlays/azure/search/search-vs.yaml index 5b98cdc..e4fffa8 100644 --- a/infra/k8s/apps/overlays/azure/search/search-vs.yaml +++ b/infra/k8s/apps/overlays/azure/search/search-vs.yaml @@ -2,7 +2,7 @@ apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: search - namespace: e-commerce + namespace: NAMESPACE_PLACEHOLDER spec: hosts: - "*" @@ -16,6 +16,6 @@ spec: uri: / route: - destination: - host: search-service.e-commerce.svc.cluster.local + host: search-service.NAMESPACE_PLACEHOLDER.svc.cluster.local port: number: 4000 \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml b/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml index 6c0fbb2..8f72f79 100644 --- a/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/store-ui/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/store-ui diff --git a/infra/k8s/apps/overlays/azure/store-ui/store-ui-vs.yaml b/infra/k8s/apps/overlays/azure/store-ui/store-ui-vs.yaml index 5851df5..3480e0c 100644 --- a/infra/k8s/apps/overlays/azure/store-ui/store-ui-vs.yaml +++ b/infra/k8s/apps/overlays/azure/store-ui/store-ui-vs.yaml @@ -2,18 +2,18 @@ apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: store-ui - namespace: e-commerce + namespace: NAMESPACE_PLACEHOLDER spec: hosts: - "*" gateways: - - aks-istio-ingress/store-ui-gateway # namespace/name + - aks-istio-ingress/store-ui-gateway # Fixed namespace for the shared gateway http: - match: # everything that hits "/" - uri: prefix: / route: - destination: - host: store-ui-service.e-commerce.svc.cluster.local + host: store-ui-service.NAMESPACE_PLACEHOLDER.svc.cluster.local port: number: 80 # servicePort in the Service object \ No newline at end of file diff --git a/infra/k8s/apps/overlays/azure/users/kustomization.yaml b/infra/k8s/apps/overlays/azure/users/kustomization.yaml index f4857c6..2c2e699 100644 --- a/infra/k8s/apps/overlays/azure/users/kustomization.yaml +++ b/infra/k8s/apps/overlays/azure/users/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/users diff --git a/infra/k8s/apps/overlays/azure/users/users-vs.yaml b/infra/k8s/apps/overlays/azure/users/users-vs.yaml index b266b22..65d2b26 100644 --- a/infra/k8s/apps/overlays/azure/users/users-vs.yaml +++ b/infra/k8s/apps/overlays/azure/users/users-vs.yaml @@ -2,7 +2,7 @@ apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: users - namespace: e-commerce + namespace: NAMESPACE_PLACEHOLDER spec: hosts: - "*" @@ -16,6 +16,6 @@ spec: uri: / route: - destination: - host: users-service.e-commerce.svc.cluster.local + host: users-service.NAMESPACE_PLACEHOLDER.svc.cluster.local port: number: 9090 \ No newline at end of file diff --git a/infra/k8s/apps/overlays/local/cart/kustomization.yaml b/infra/k8s/apps/overlays/local/cart/kustomization.yaml index af33cf8..42341b4 100644 --- a/infra/k8s/apps/overlays/local/cart/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/cart/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/cart - service-nodeport.yaml diff --git a/infra/k8s/apps/overlays/local/products/kustomization.yaml b/infra/k8s/apps/overlays/local/products/kustomization.yaml index 01424ab..82acbbb 100644 --- a/infra/k8s/apps/overlays/local/products/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/products/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/products - service-nodeport.yaml @@ -14,7 +14,7 @@ configMapGenerator: - behavior: create literals: - MONGO_URI=mongodb://mongo-root-username:mongo-root-password@mongo-service.shared-services.svc.cluster.local:27017/ - - DATABASE=e-commerce + - DATABASE=NAMESPACE_PLACEHOLDER name: products-configmap generatorOptions: disableNameSuffixHash: true diff --git a/infra/k8s/apps/overlays/local/search/kustomization.yaml b/infra/k8s/apps/overlays/local/search/kustomization.yaml index 7037d97..de19898 100644 --- a/infra/k8s/apps/overlays/local/search/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/search/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/search - service-nodeport.yaml diff --git a/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml b/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml index a5f609c..31c32be 100644 --- a/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/store-ui/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/store-ui diff --git a/infra/k8s/apps/overlays/local/users/kustomization.yaml b/infra/k8s/apps/overlays/local/users/kustomization.yaml index 3e321b4..1968f53 100644 --- a/infra/k8s/apps/overlays/local/users/kustomization.yaml +++ b/infra/k8s/apps/overlays/local/users/kustomization.yaml @@ -1,6 +1,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e-commerce +namespace: NAMESPACE_PLACEHOLDER resources: - ../../../base/users - service-nodeport.yaml @@ -14,7 +14,7 @@ configMapGenerator: - behavior: create literals: - MONGO_URI=mongodb://mongo-root-username:mongo-root-password@mongo-service.shared-services.svc.cluster.local:27017/ - - DATABASE=e-commerce + - DATABASE=NAMESPACE_PLACEHOLDER name: users-configmap generatorOptions: disableNameSuffixHash: true diff --git a/infra/k8s/deploy-with-namespace.sh b/infra/k8s/deploy-with-namespace.sh new file mode 100644 index 0000000..deff9cf --- /dev/null +++ b/infra/k8s/deploy-with-namespace.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# This script deploys the e-commerce application to a user-specific namespace + +# Use current username as default namespace if not provided, but sanitize it for Kubernetes +DEFAULT_NAMESPACE=$(echo "$USER" | tr '.' '-') +USER_NAMESPACE=${1:-$DEFAULT_NAMESPACE} +ENVIRONMENT=${2:-"azure"} # Default to azure if not specified +BASEDIR=$(dirname "$0") + +echo "Deploying to namespace: $USER_NAMESPACE" +echo "Environment: $ENVIRONMENT" + +if [ "$ENVIRONMENT" != "azure" ] && [ "$ENVIRONMENT" != "local" ]; then + echo "Environment must be either 'azure' or 'local'" + exit 1 +fi + +# Create a temporary directory for the customized yaml +TEMP_DIR=$(mktemp -d) +echo "Creating temporary files in $TEMP_DIR" + +# Copy the kustomization files to the temp directory (both base and overlays) +mkdir -p $TEMP_DIR/base $TEMP_DIR/overlays +cp -r $BASEDIR/apps/base/* $TEMP_DIR/base/ +cp -r $BASEDIR/apps/overlays/$ENVIRONMENT $TEMP_DIR/overlays/ + +# Replace the namespace placeholder in all kustomization.yaml files (both base and overlays) +find $TEMP_DIR -name "kustomization.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/$USER_NAMESPACE/g" {} \; + +# Replace the namespace placeholder in all VirtualService files +find $TEMP_DIR -name "*-vs.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/$USER_NAMESPACE/g" {} \; + +# Create the namespace if it doesn't exist +kubectl create namespace $USER_NAMESPACE --dry-run=client -o yaml | kubectl apply -f - + +# Apply the customized kustomization +kubectl apply -k $TEMP_DIR/overlays/$ENVIRONMENT + +# If it's Azure environment, handle the Istio Gateway +if [ "$ENVIRONMENT" == "azure" ]; then + # Check if the Istio Gateway exists in the aks-istio-ingress namespace + GATEWAY_EXISTS=$(kubectl get gateway -n aks-istio-ingress store-ui-gateway -o name --ignore-not-found) + + if [ -z "$GATEWAY_EXISTS" ]; then + echo "Creating Istio Gateway in aks-istio-ingress namespace" + # Apply the Gateway configuration separately since it's in a different namespace + cp $BASEDIR/apps/overlays/azure/store-ui/store-ui-gateway.yaml $TEMP_DIR/ + kubectl apply -f $TEMP_DIR/store-ui-gateway.yaml + else + echo "Istio Gateway already exists in aks-istio-ingress namespace" + fi +fi + +# Clean up +rm -rf $TEMP_DIR + +echo "Application deployed to namespace: $USER_NAMESPACE" +if [ "$ENVIRONMENT" == "azure" ]; then + echo "Access your application through the Istio Gateway" +else + echo "Access your application through NodePort services" +fi \ No newline at end of file diff --git a/infra/k8s/install.sh b/infra/k8s/install.sh new file mode 100755 index 0000000..653389a --- /dev/null +++ b/infra/k8s/install.sh @@ -0,0 +1,147 @@ +#!/bin/bash + +# This script installs the e-commerce application to a user-specific namespace + +# Use current username as default namespace if not provided, but sanitize it for Kubernetes +DEFAULT_NAMESPACE=$(echo "$USER" | tr '.' '-') +USER_NAMESPACE=${1:-$DEFAULT_NAMESPACE} +ENVIRONMENT=${2:-"local"} # Default to local if not specified +BASEDIR=$(dirname "$0") + +# Define emojis for better UX +ROCKET="๐Ÿš€" +CHECK="โœ…" +WARN="โš ๏ธ" +INFO="โ„น๏ธ" +ERROR="โŒ" +TIMER="โฑ๏ธ" +DATABASE="๐Ÿ—„๏ธ" +CLOUD="โ˜๏ธ" +SERVER="๐Ÿ–ฅ๏ธ" +CLEANUP="๐Ÿงน" + +echo "$ROCKET Starting installation to namespace: $USER_NAMESPACE" +echo "$INFO Environment: $ENVIRONMENT" + +# Track start time for performance monitoring +START_TIME=$(date +%s) + +if [ "$ENVIRONMENT" != "azure" ] && [ "$ENVIRONMENT" != "local" ]; then + echo "$ERROR Environment must be either 'azure' or 'local'" + exit 1 +fi + +# Check for Istio CRDs if using azure environment +if [ "$ENVIRONMENT" == "azure" ]; then + echo "$INFO Checking for Istio CRDs..." + if ! kubectl get crd gateways.networking.istio.io &> /dev/null; then + echo "$WARN Istio CRDs not found. Some Istio features may not work properly." + echo "$INFO You may need to install Istio: https://istio.io/latest/docs/setup/getting-started/" + else + echo "$CHECK Istio CRDs found" + fi +fi + +# Determine application services list +SERVICES=("store-ui" "products" "cart" "users" "search") + +# Create a temporary directory for the customized yaml +TEMP_DIR=$(mktemp -d) +echo "$INFO Creating temporary files in $TEMP_DIR" + +# Copy the kustomization files to the temp directory (both base and overlays) +mkdir -p $TEMP_DIR/base $TEMP_DIR/overlays +cp -r $BASEDIR/apps/base/* $TEMP_DIR/base/ +cp -r $BASEDIR/apps/overlays/$ENVIRONMENT $TEMP_DIR/overlays/ + +# Replace the namespace placeholder in all kustomization.yaml files (both base and overlays) +find $TEMP_DIR -name "kustomization.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/$USER_NAMESPACE/g" {} \; + +# Replace the namespace placeholder in all VirtualService files +find $TEMP_DIR -name "*-vs.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/$USER_NAMESPACE/g" {} \; + +# Replace the namespace placeholder in any other yaml files +find $TEMP_DIR -name "*.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/$USER_NAMESPACE/g" {} \; + +# Create the namespace if it doesn't exist +echo "$INFO Creating namespace: $USER_NAMESPACE" +kubectl create namespace $USER_NAMESPACE --dry-run=client -o yaml | kubectl apply -f - +echo "$CHECK Namespace ready" + +# Create shared services first +echo "$DATABASE Setting up shared services..." +kubectl apply -k $BASEDIR/shared-services/overlays/$ENVIRONMENT --wait + +# Wait for shared services to be ready +echo "$TIMER Waiting for shared services to become ready..." +kubectl wait --for=condition=ready pod -l app=mongodb -n shared-services --timeout=120s && echo "$CHECK MongoDB ready" || echo "$WARN MongoDB may not be ready yet" +kubectl wait --for=condition=ready pod -l app=redis -n shared-services --timeout=120s && echo "$CHECK Redis ready" || echo "$WARN Redis may not be ready yet" +kubectl wait --for=condition=ready pod -l app=elasticsearch -n shared-services --timeout=120s && echo "$CHECK Elasticsearch ready" || echo "$WARN Elasticsearch may not be ready yet" + +# Progress tracking +TOTAL=${#SERVICES[@]} +CURRENT=0 + +# Apply each service separately for better visibility +for SERVICE in "${SERVICES[@]}"; do + CURRENT=$((CURRENT+1)) + PROGRESS=$((CURRENT*100/TOTAL)) + echo "$SERVER Installing $SERVICE service... ($CURRENT/$TOTAL - $PROGRESS%)" + if [ -d "$TEMP_DIR/overlays/$ENVIRONMENT/$SERVICE" ]; then + # Use --server-side to avoid client-side validation issues + kubectl apply -k $TEMP_DIR/overlays/$ENVIRONMENT/$SERVICE --server-side || echo "$WARN Some resources for $SERVICE might not have been applied successfully" + echo "$CHECK $SERVICE installed" + else + echo "$WARN $SERVICE directory not found in $ENVIRONMENT overlay" + fi +done + +# If it's Azure environment, handle the Istio Gateway +if [ "$ENVIRONMENT" == "azure" ]; then + # Check if the Istio Gateway exists in the aks-istio-ingress namespace + GATEWAY_EXISTS=$(kubectl get gateway -n aks-istio-ingress store-ui-gateway -o name --ignore-not-found) + + if [ -z "$GATEWAY_EXISTS" ]; then + echo "$INFO Creating Istio Gateway in aks-istio-ingress namespace" + # Apply the Gateway configuration separately since it's in a different namespace + cp $BASEDIR/apps/overlays/azure/store-ui/store-ui-gateway.yaml $TEMP_DIR/ + # Use --server-side to avoid client-side validation issues + kubectl apply -f $TEMP_DIR/store-ui-gateway.yaml --server-side || echo "$WARN Gateway might not have been applied successfully" + echo "$CHECK Gateway installed" + else + echo "$INFO Istio Gateway already exists in aks-istio-ingress namespace" + fi +fi + +# Clean up +rm -rf $TEMP_DIR +echo "$CLEANUP Temporary files cleaned up" + +# Calculate elapsed time +END_TIME=$(date +%s) +ELAPSED=$((END_TIME - START_TIME)) +MINUTES=$((ELAPSED / 60)) +SECONDS=$((ELAPSED % 60)) + +echo "$ROCKET Application successfully installed to namespace: $USER_NAMESPACE" +echo "$TIMER Installation completed in ${MINUTES}m ${SECONDS}s" + +if [ "$ENVIRONMENT" == "azure" ]; then + echo "$CLOUD Access your application through the Istio Gateway" + + # Get Istio ingress IP for convenience + INGRESS_IP=$(kubectl get svc -n aks-istio-ingress aks-istio-ingressgateway-external -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null) + if [ ! -z "$INGRESS_IP" ]; then + echo "$INFO Istio Gateway IP: $INGRESS_IP" + echo "$INFO You can access the application at: http://$INGRESS_IP/" + fi +else + echo "$SERVER Access your application through NodePort services" + + # Get NodePort for store-ui for convenience + NODE_PORT=$(kubectl get svc -n $USER_NAMESPACE store-ui-service -o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null) + if [ ! -z "$NODE_PORT" ]; then + echo "$INFO Store UI NodePort: $NODE_PORT" + echo "$INFO You can access the application at: http://localhost:$NODE_PORT/" + fi +fi \ No newline at end of file diff --git a/infra/k8s/uninstall.sh b/infra/k8s/uninstall.sh new file mode 100755 index 0000000..75b8891 --- /dev/null +++ b/infra/k8s/uninstall.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +# This script uninstalls the e-commerce application from a user-specific namespace + +# Use current username as default namespace if not provided, but sanitize it for Kubernetes +DEFAULT_NAMESPACE=$(echo "$USER" | tr '.' '-') +USER_NAMESPACE=${1:-$DEFAULT_NAMESPACE} +ENVIRONMENT=${2:-"local"} # Default to local if not specified +REMOVE_SHARED=${3:-"false"} # Whether to remove shared services too, default is false +BASEDIR=$(dirname "$0") + +# Define emojis for better UX +ROCKET="๐Ÿš€" +CHECK="โœ…" +WARN="โš ๏ธ" +INFO="โ„น๏ธ" +ERROR="โŒ" +TIMER="โฑ๏ธ" +DATABASE="๐Ÿ—„๏ธ" +CLOUD="โ˜๏ธ" +SERVER="๐Ÿ–ฅ๏ธ" +CLEANUP="๐Ÿงน" +TRASH="๐Ÿ—‘๏ธ" + +echo "$TRASH Starting uninstallation from namespace: $USER_NAMESPACE" +echo "$INFO Environment: $ENVIRONMENT" + +# Track start time for performance monitoring +START_TIME=$(date +%s) + +if [ "$ENVIRONMENT" != "azure" ] && [ "$ENVIRONMENT" != "local" ]; then + echo "$ERROR Environment must be either 'azure' or 'local'" + exit 1 +fi + +# Check for Istio CRDs if using azure environment +if [ "$ENVIRONMENT" == "azure" ]; then + echo "$INFO Checking for Istio CRDs..." + if ! kubectl get crd gateways.networking.istio.io &> /dev/null; then + echo "$WARN Istio CRDs not found. Skipping Istio resource deletion." + SKIP_ISTIO=true + else + echo "$CHECK Istio CRDs found" + SKIP_ISTIO=false + fi +fi + +# Determine application services list in reverse order for proper removal +SERVICES=("store-ui" "search" "users" "cart" "products") + +# Create a temporary directory for the customized yaml +TEMP_DIR=$(mktemp -d) +echo "$INFO Creating temporary files in $TEMP_DIR" + +# Copy the kustomization files to the temp directory (both base and overlays) +mkdir -p $TEMP_DIR/base $TEMP_DIR/overlays +cp -r $BASEDIR/apps/base/* $TEMP_DIR/base/ +cp -r $BASEDIR/apps/overlays/$ENVIRONMENT $TEMP_DIR/overlays/ + +# Replace the namespace placeholder in all kustomization.yaml files (both base and overlays) +find $TEMP_DIR -name "kustomization.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/$USER_NAMESPACE/g" {} \; + +# Replace the namespace placeholder in all VirtualService files +find $TEMP_DIR -name "*-vs.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/$USER_NAMESPACE/g" {} \; + +# Replace the namespace placeholder in any other yaml files +find $TEMP_DIR -name "*.yaml" -exec sed -i "s/NAMESPACE_PLACEHOLDER/$USER_NAMESPACE/g" {} \; + +# If Istio CRDs are missing, remove VirtualService and Gateway yamls to avoid errors +if [ "$SKIP_ISTIO" = true ]; then + echo "$INFO Removing Istio resources from manifests to avoid errors..." + find $TEMP_DIR -name "*-vs.yaml" -delete + find $TEMP_DIR -name "*-gateway.yaml" -delete +fi + +# Progress tracking +TOTAL=${#SERVICES[@]} +CURRENT=0 + +# Remove each service separately for better visibility (in reverse order) +for SERVICE in "${SERVICES[@]}"; do + CURRENT=$((CURRENT+1)) + PROGRESS=$((CURRENT*100/TOTAL)) + echo "$TRASH Uninstalling $SERVICE service... ($CURRENT/$TOTAL - $PROGRESS%)" + + if [ -d "$TEMP_DIR/overlays/$ENVIRONMENT/$SERVICE" ]; then + # For extra safety, explicitly delete deployments and services first + kubectl delete deployment -n $USER_NAMESPACE -l app=${SERVICE}-deployment --ignore-not-found + kubectl delete service -n $USER_NAMESPACE ${SERVICE}-service --ignore-not-found + + # Then try the kustomize approach for anything else + kubectl delete -k $TEMP_DIR/overlays/$ENVIRONMENT/$SERVICE --ignore-not-found + + echo "$CHECK $SERVICE uninstalled" + else + echo "$WARN $SERVICE directory not found in $ENVIRONMENT overlay" + fi +done + +# If it's Azure environment, handle the Istio Gateway if needed +if [ "$ENVIRONMENT" == "azure" ] && [ "$SKIP_ISTIO" = false ]; then + # We generally don't delete the shared Istio Gateway unless explicitly requested + if [ "$REMOVE_SHARED" == "true" ]; then + echo "$INFO Removing Istio Gateway from aks-istio-ingress namespace" + GATEWAY_FILE="$BASEDIR/apps/overlays/azure/store-ui/store-ui-gateway.yaml" + if [ -f "$GATEWAY_FILE" ]; then + kubectl delete -f "$GATEWAY_FILE" --ignore-not-found + echo "$CHECK Gateway removed" + fi + fi +fi + +# Remove shared services if explicitly requested +if [ "$REMOVE_SHARED" == "true" ]; then + echo "$DATABASE Removing shared services..." + kubectl delete -k $BASEDIR/shared-services/overlays/$ENVIRONMENT --ignore-not-found + echo "$CHECK Shared services removed" +fi + +# Remove the namespace itself +echo "$TRASH Removing namespace: $USER_NAMESPACE" +kubectl delete namespace $USER_NAMESPACE --ignore-not-found +echo "$CHECK Namespace removed" + +# Clean up +rm -rf $TEMP_DIR +echo "$CLEANUP Temporary files cleaned up" + +# Calculate elapsed time +END_TIME=$(date +%s) +ELAPSED=$((END_TIME - START_TIME)) +MINUTES=$((ELAPSED / 60)) +SECONDS=$((ELAPSED % 60)) + +echo "$ROCKET Application successfully uninstalled from namespace: $USER_NAMESPACE" +echo "$TIMER Uninstallation completed in ${MINUTES}m ${SECONDS}s" + +if [ "$REMOVE_SHARED" == "true" ]; then + echo "$DATABASE Shared services have also been removed" +else + echo "$INFO Shared services were preserved" + echo "$INFO To remove shared services, run with: $0 $USER_NAMESPACE $ENVIRONMENT true" +fi \ No newline at end of file diff --git a/products-cna-microservice/devspace.yaml b/products-cna-microservice/devspace.yaml index 1b473e2..fe92190 100644 --- a/products-cna-microservice/devspace.yaml +++ b/products-cna-microservice/devspace.yaml @@ -8,9 +8,9 @@ vars: SERVICE_NAME: default: "products" K8S_MANIFEST_PATH_LOCAL: - default: "../infra/k8s/apps/overlays/local/products/" + default: "${K8S_DIR}/infra/k8s/apps/overlays/local/products/" K8S_MANIFEST_PATH_AZURE: - default: "../infra/k8s/apps/overlays/azure/products/" + default: "${K8S_DIR}/infra/k8s/apps/overlays/azure/products/" dev: products: diff --git a/search-cna-microservice/devspace.yaml b/search-cna-microservice/devspace.yaml index 7b0fe1f..923c81b 100644 --- a/search-cna-microservice/devspace.yaml +++ b/search-cna-microservice/devspace.yaml @@ -8,9 +8,9 @@ vars: SERVICE_NAME: default: "search" K8S_MANIFEST_PATH_LOCAL: - default: "../infra/k8s/apps/overlays/local/search/" + default: "${K8S_DIR}/infra/k8s/apps/overlays/local/search/" K8S_MANIFEST_PATH_AZURE: - default: "../infra/k8s/apps/overlays/azure/search/" + default: "${K8S_DIR}/infra/k8s/apps/overlays/azure/search/" dev: search: diff --git a/store-ui/devspace.yaml b/store-ui/devspace.yaml index 0f94da8..3aaff30 100644 --- a/store-ui/devspace.yaml +++ b/store-ui/devspace.yaml @@ -8,9 +8,9 @@ vars: SERVICE_NAME: default: "store-ui" K8S_MANIFEST_PATH_LOCAL: - default: "../infra/k8s/apps/overlays/local/store-ui/" + default: "${K8S_DIR}/infra/k8s/apps/overlays/local/store-ui/" K8S_MANIFEST_PATH_AZURE: - default: "../infra/k8s/apps/overlays/azure/store-ui/" + default: "${K8S_DIR}/infra/k8s/apps/overlays/azure/store-ui/" dev: store-ui: diff --git a/users-cna-microservice/devspace.yaml b/users-cna-microservice/devspace.yaml index 7fa29df..4aecccc 100755 --- a/users-cna-microservice/devspace.yaml +++ b/users-cna-microservice/devspace.yaml @@ -8,9 +8,9 @@ vars: SERVICE_NAME: default: "users" K8S_MANIFEST_PATH_LOCAL: - default: "../infra/k8s/apps/overlays/local/users/" + default: "${K8S_DIR}/infra/k8s/apps/overlays/local/users/" K8S_MANIFEST_PATH_AZURE: - default: "../infra/k8s/apps/overlays/azure/users/" + default: "${K8S_DIR}/infra/k8s/apps/overlays/azure/users/" dev: users: From e0dd71f5f48d16188bc36de3e4d35b99371cbd38 Mon Sep 17 00:00:00 2001 From: "naveenkumar.kumanan" Date: Mon, 14 Jul 2025 11:39:56 +0530 Subject: [PATCH 06/15] Improved UX --- store-ui/package-lock.json | 86 +- store-ui/package.json | 2 + store-ui/src/App.tsx | 310 ++++- store-ui/src/components/AppBar/AppBar.tsx | 880 +++++++------- store-ui/src/components/Deals/Deals.tsx | 450 +++++++- store-ui/src/components/ImageOptimizer.tsx | 190 +++ store-ui/src/components/SEO.tsx | 102 ++ store-ui/src/components/SearchComponent.tsx | 549 ++++----- .../src/components/layout/CartContext.tsx | 234 +++- .../src/components/layout/Header/Header.tsx | 323 +++++- store-ui/src/components/layout/Layout.tsx | 268 ++++- .../src/components/layout/ThemeContext.tsx | 64 +- store-ui/src/index.tsx | 67 +- store-ui/src/pages/Cart/Cart.tsx | 56 +- store-ui/src/pages/Checkout/Checkout.tsx | 1026 ++++++++++++++--- store-ui/src/pages/Home/Home.test.tsx | 31 +- store-ui/src/pages/Home/Home.tsx | 104 +- .../OrderConfirmation/OrderConfirmation.tsx | 54 +- store-ui/src/pages/Product/Product.tsx | 856 ++++++++++++-- 19 files changed, 4500 insertions(+), 1152 deletions(-) create mode 100644 store-ui/src/components/ImageOptimizer.tsx create mode 100644 store-ui/src/components/SEO.tsx diff --git a/store-ui/package-lock.json b/store-ui/package-lock.json index 1dc815d..73b7e64 100644 --- a/store-ui/package-lock.json +++ b/store-ui/package-lock.json @@ -20,10 +20,12 @@ "@types/node": "^16.11.41", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", + "@types/react-helmet-async": "^1.0.3", "axios": "^0.27.2", "framer-motion": "^12.19.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-helmet-async": "^2.0.5", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "typescript": "^4.7.4", @@ -4144,6 +4146,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-helmet-async": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/react-helmet-async/-/react-helmet-async-1.0.3.tgz", + "integrity": "sha512-DqbSuZPSHiH1l3XI/y8LbhrAGNh+Bpc9QY4MsYRM1yD4+qhax8bN4DInUMpv/tNyIdjsa+1V8XXmbRx8W5dB0w==", + "deprecated": "This is a stub types definition. react-helmet-async provides its own type definitions, so you do not need this installed.", + "license": "MIT", + "dependencies": { + "react-helmet-async": "*" + } + }, "node_modules/@types/react-is": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", @@ -9133,6 +9145,15 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ipaddr.js": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", @@ -14270,6 +14291,26 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-2.0.5.tgz", + "integrity": "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==", + "license": "Apache-2.0", + "dependencies": { + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -15107,6 +15148,12 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16289,7 +16336,8 @@ "node_modules/web-vitals": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", - "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" + "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==", + "license": "Apache-2.0" }, "node_modules/webidl-conversions": { "version": "6.1.0", @@ -19955,6 +20003,14 @@ "@types/react": "*" } }, + "@types/react-helmet-async": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/react-helmet-async/-/react-helmet-async-1.0.3.tgz", + "integrity": "sha512-DqbSuZPSHiH1l3XI/y8LbhrAGNh+Bpc9QY4MsYRM1yD4+qhax8bN4DInUMpv/tNyIdjsa+1V8XXmbRx8W5dB0w==", + "requires": { + "react-helmet-async": "*" + } + }, "@types/react-is": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", @@ -23595,6 +23651,14 @@ "side-channel": "^1.0.4" } }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "ipaddr.js": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", @@ -27142,6 +27206,21 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "react-helmet-async": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-2.0.5.tgz", + "integrity": "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==", + "requires": { + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "shallowequal": "^1.1.0" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -27759,6 +27838,11 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/store-ui/package.json b/store-ui/package.json index 98754a6..5c8fc0d 100644 --- a/store-ui/package.json +++ b/store-ui/package.json @@ -15,10 +15,12 @@ "@types/node": "^16.11.41", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", + "@types/react-helmet-async": "^1.0.3", "axios": "^0.27.2", "framer-motion": "^12.19.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-helmet-async": "^2.0.5", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "typescript": "^4.7.4", diff --git a/store-ui/src/App.tsx b/store-ui/src/App.tsx index 2d44952..916793c 100644 --- a/store-ui/src/App.tsx +++ b/store-ui/src/App.tsx @@ -1,38 +1,274 @@ -import { BrowserRouter, Routes, Route } from "react-router-dom" -import Home from './pages/Home/Home' -import Product from './pages/Product/Product' -import Cart from './pages/Cart/Cart' -import Checkout from './pages/Checkout/Checkout' -import OrderConfirmation from './pages/OrderConfirmation/OrderConfirmation' -import UserPage from './pages/User/UserPage' -import UserListPage from './pages/User/UserListPage' -import { LoginPage, RegisterPage, ProfilePage } from './pages/Auth' -import { AdminDashboard } from './pages/Admin' -import Layout from './components/layout/Layout' -import { AuthProvider } from './contexts/AuthContext' -import SearchComponent from './components/SearchComponent' - -const App = (props: any) => { - return ( - - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - ) +import React, { Suspense } from 'react'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { HelmetProvider } from 'react-helmet-async'; +import Layout from './components/layout/Layout'; +import { AuthProvider } from './contexts/AuthContext'; +import CircularProgress from '@mui/material/CircularProgress'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Alert from '@mui/material/Alert'; + +// Lazy load components for code splitting +const Home = React.lazy(() => import('./pages/Home/Home')); +const SearchComponent = React.lazy(() => import('./components/SearchComponent')); +const Product = React.lazy(() => import('./pages/Product/Product')); +const Cart = React.lazy(() => import('./pages/Cart/Cart')); +const Checkout = React.lazy(() => import('./pages/Checkout/Checkout')); +const OrderConfirmation = React.lazy(() => import('./pages/OrderConfirmation/OrderConfirmation')); + +// Auth pages +const LoginPage = React.lazy(() => import('./pages/Auth/LoginPage')); +const RegisterPage = React.lazy(() => import('./pages/Auth/RegisterPage')); +const ProfilePage = React.lazy(() => import('./pages/Auth/ProfilePage')); + +// Admin pages with error boundary +const AdminDashboard = React.lazy(() => import('./pages/Admin/AdminDashboard')); + +// User management pages +const UserPage = React.lazy(() => import('./pages/User/UserPage')); +const UserListPage = React.lazy(() => import('./pages/User/UserListPage')); + +// Enhanced Loading component with better UX +const LoadingFallback: React.FC<{ message?: string }> = ({ message = 'Loading...' }) => ( + + + + {message} + + +); + +// Error Boundary Component +class ErrorBoundary extends React.Component< + { children: React.ReactNode; fallback?: React.ReactNode }, + { hasError: boolean; error?: Error } +> { + constructor(props: { children: React.ReactNode; fallback?: React.ReactNode }) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('Error caught by boundary:', error, errorInfo); + + // In production, send error to monitoring service + if (process.env.NODE_ENV === 'production') { + // Example: Sentry.captureException(error); + } + } + + render() { + if (this.state.hasError) { + return this.props.fallback || ( + + + + Something went wrong + + + We're sorry, but something unexpected happened. Please try refreshing the page. + + + + + + + ); + } + + return this.props.children; + } } -export default App \ No newline at end of file + +// Enhanced Suspense wrapper with error boundary +const SuspenseWrapper: React.FC<{ children: React.ReactNode; fallback?: React.ReactNode }> = ({ + children, + fallback +}) => ( + + }> + {children} + + +); + +function App() { + return ( + + + + + + {/* Home page - highest priority */} + }> + + + } + /> + + {/* Search page */} + }> + + + } + /> + + {/* Product pages */} + }> + + + } + /> + + {/* Cart and checkout flow */} + }> + + + } + /> + + }> + + + } + /> + + }> + + + } + /> + + {/* Auth pages - separate chunk */} + }> + + + } + /> + + }> + + + } + /> + + }> + + + } + /> + + {/* Admin pages - separate chunk for role-based access */} + }> + + + } + /> + + {/* User management pages */} + }> + + + } + /> + + }> + + + } + /> + + {/* Category routes - can be dynamically loaded */} + }> + + + } + /> + + {/* 404 fallback */} + + + 404 - Page Not Found + + + The page you're looking for doesn't exist. + + + } + /> + + + + + + ); +} + +export default App; \ No newline at end of file diff --git a/store-ui/src/components/AppBar/AppBar.tsx b/store-ui/src/components/AppBar/AppBar.tsx index 45880e8..63d0be0 100644 --- a/store-ui/src/components/AppBar/AppBar.tsx +++ b/store-ui/src/components/AppBar/AppBar.tsx @@ -24,7 +24,8 @@ import { alpha, Collapse, Fade, - Zoom + Zoom, + Chip } from '@mui/material'; import { styled, useTheme } from '@mui/material/styles'; import { @@ -44,7 +45,8 @@ import { Storefront as StorefrontIcon, Phone as PhoneIcon, Help as HelpIcon, - LocalShipping as LocalShippingIcon + LocalShipping as LocalShippingIcon, + Close as CloseIcon } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; import { useCart } from '../layout/CartContext'; @@ -54,27 +56,24 @@ import ThemeContext from '../layout/ThemeContext'; // Styled components const StyledAppBar = styled(MuiAppBar)(({ theme }) => ({ backdropFilter: 'blur(10px)', - backgroundColor: alpha(theme.palette.background.paper, 0.9), - boxShadow: '0 4px 20px rgba(0,0,0,0.07)', + backgroundColor: '#263238', // Dark navy/gray color from screenshot + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', borderBottom: `1px solid ${alpha(theme.palette.divider, 0.1)}`, - color: theme.palette.text.primary, + color: '#FFFFFF', transition: 'all 0.3s ease' })); const Logo = styled(Typography)(({ theme }) => ({ - fontWeight: 700, + fontWeight: 600, letterSpacing: '0.5px', - background: theme.palette.mode === 'dark' - ? 'linear-gradient(45deg, #f06, #3cf)' - : 'linear-gradient(45deg, #3f51b5, #f50057)', - WebkitBackgroundClip: 'text', - WebkitTextFillColor: 'transparent', - fontSize: '1.5rem', + color: '#FF9800', // Orange color from the screenshot + fontSize: '1.4rem', display: 'flex', alignItems: 'center', - textTransform: 'uppercase' + cursor: 'pointer' })); +// Add keyboard focus styles to NavLink const NavLink = styled(Button)(({ theme }) => ({ textTransform: 'none', fontWeight: 500, @@ -91,8 +90,12 @@ const NavLink = styled(Button)(({ theme }) => ({ transition: 'all 0.3s ease', transform: 'translateX(-50%)', }, - '&:hover::after': { + '&:hover::after, &:focus-visible::after': { width: '70%', + }, + '&:focus-visible': { + outline: `2px solid ${theme.palette.primary.main}`, + outlineOffset: '2px', } })); @@ -136,10 +139,15 @@ const StyledInputBase = styled(InputBase)(({ theme }) => ({ }, })); +// Add keyboard focus styles to ProfileButton const ProfileButton = styled(IconButton)(({ theme }) => ({ transition: 'transform 0.2s ease', '&:hover': { transform: 'scale(1.05)', + }, + '&:focus-visible': { + outline: `2px solid ${theme.palette.primary.main}`, + outlineOffset: '2px', } })); @@ -189,6 +197,15 @@ const MenuButton = styled(Button)(({ theme }) => ({ } })); +// Enhance the ListItemButton keyboard accessibility +const StyledListItemButton = styled(ListItemButton)(({ theme }) => ({ + '&:focus-visible': { + outline: `2px solid ${theme.palette.primary.main}`, + outlineOffset: '-2px', + backgroundColor: alpha(theme.palette.primary.main, 0.1), + } +})); + const AppBar = () => { const navigate = useNavigate(); const { cartCount } = useCart(); @@ -214,6 +231,13 @@ const AppBar = () => { const [openCategories, setOpenCategories] = useState(false); const [openHelp, setOpenHelp] = useState(false); + // Add this at the beginning of the component + const [searchQuery, setSearchQuery] = useState(''); + const searchInputRef = React.useRef(null); + + // Add state for screen reader announcements + const [announcement, setAnnouncement] = useState(''); + const handleProfileClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -264,29 +288,105 @@ const AppBar = () => { { name: 'FAQ', path: '/help/faq' } ]; + // Add keyboard shortcut for search focus + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + // Focus search input when user presses '/' key + if (event.key === '/' && document.activeElement?.tagName !== 'INPUT') { + event.preventDefault(); + searchInputRef.current?.focus(); + setAnnouncement('Search box is now focused. Type to search products.'); + } + + // Add shortcut to go to cart (c key) + if (event.key === 'c' && (event.ctrlKey || event.metaKey) && document.activeElement?.tagName !== 'INPUT') { + event.preventDefault(); + navigate('/cart'); + setAnnouncement('Navigated to shopping cart'); + } + + // Add shortcut to go to home (h key) + if (event.key === 'h' && (event.ctrlKey || event.metaKey) && document.activeElement?.tagName !== 'INPUT') { + event.preventDefault(); + navigate('/'); + setAnnouncement('Navigated to home page'); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [navigate]); + + // Clear announcement after it's been read + useEffect(() => { + if (announcement) { + const timer = setTimeout(() => { + setAnnouncement(''); + }, 3000); + + return () => clearTimeout(timer); + } + }, [announcement]); + + // Handle search submission + const handleSearchSubmit = (event: React.FormEvent) => { + event.preventDefault(); + if (searchQuery.trim()) { + navigate(`/search?q=${encodeURIComponent(searchQuery)}`); + setSearchQuery(''); + setDrawerOpen(false); + } + }; + const renderMobileDrawer = () => ( - + handleNavigation('/')}> EShop + + + {isLoggedIn && ( - + {getInitial()} @@ -302,133 +402,153 @@ const AppBar = () => { )} - + - handleNavigation('/')}> + handleNavigation('/')} + aria-label="Home page" + sx={{ py: 1 }} + > - + - setOpenCategories(!openCategories)}> + setOpenCategories(!openCategories)} + aria-expanded={openCategories} + aria-controls="categories-submenu" + sx={{ py: 1 }} + > - {openCategories ? : } - + {openCategories ? - - + + {categories.map((category) => ( - handleNavigation(category.path)} + aria-label={`Browse ${category.name} category`} > - - - ))} - - - - - setOpenHelp(!openHelp)}> - - - - - {openHelp ? : } - - - - - - {helpLinks.map((link) => ( - - handleNavigation(link.path)} - > - - + ))} - - handleNavigation('/contact')}> - - - - - - - {isLoggedIn ? ( <> - handleNavigation('/profile')}> + handleNavigation('/profile')} + sx={{ py: 1 }} + > - + - handleNavigation('/orders')}> + handleNavigation('/orders')} + sx={{ py: 1 }} + > - + + + + + handleNavigation('/cart')} + sx={{ py: 1 }} + > + + + + + My Cart + {cartCount > 0 && ( + + )} + + } + /> + {user?.role === 'admin' || user?.role === 'ADMIN' || user?.role === 'Admin' ? ( - handleNavigation('/admin')}> + handleNavigation('/admin')} + sx={{ py: 1 }} + > - + ) : null} - + - + ) : ( <> - handleNavigation('/login')}> + handleNavigation('/login')} + sx={{ py: 1 }} + > - + - handleNavigation('/register')}> + handleNavigation('/register')} + sx={{ py: 1 }} + > - + )} @@ -450,371 +570,339 @@ const AppBar = () => { ); return ( - - - - - {isMobile && ( + + {/* Add live region for screen reader announcements */} +
+ {announcement} +
+ + + + {isMobile ? ( + <> - )} - - - + navigate('/')} + tabIndex={0} + role="button" + aria-label="Go to homepage" + sx={{ fontSize: '1.25rem' }} + > + EShop + +
+ + + navigate('/cart')} + aria-label={`Shopping Cart with ${cartCount} items`} + size="small" + sx={{ ml: 0.5 }} + > + + + + + + + ) : ( + <> + navigate('/')} + sx={{ mr: 2, cursor: 'pointer' }} + tabIndex={0} + role="button" + aria-label="Go to homepage" + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + navigate('/'); + } }} - onClick={() => navigate('/')} > EShop - - - {!isMobile && ( - <> - - navigate('/')}>Home - - - } - onClick={() => setOpenCategories(!openCategories)} - > - Categories - - {openCategories && ( - - - {categories.map((category) => ( - { - navigate(category.path); - setOpenCategories(false); - }} - sx={{ py: 1.5 }} - > - {category.name} - - ))} - - - )} - - - navigate('/deals')}>Deals - navigate('/new-arrivals')}>New Arrivals - - - } - onClick={() => setOpenHelp(!openHelp)} - > - Help - - {openHelp && ( - - - {helpLinks.map((link) => ( - { - navigate(link.path); - setOpenHelp(false); - }} - sx={{ py: 1.5 }} - > - {link.name} - - ))} - - - )} - - + + + navigate('/')} + aria-label="Home page" + > + Home + + {/* Add keyboard accessibility to the menu items */} + {categories.map((category) => ( + navigate(category.path)} + aria-label={`Browse ${category.name} category`} + > + {category.name} + + ))} + + +
- + setSearchQuery(e.target.value)} /> + {/* Add a tooltip to show the keyboard shortcut */} + - - )} - - - - - {!isMobile && ( - + + + + {theme.palette.mode === 'dark' ? : } - )} - - {/* Admin Users Icon - Only visible to admin users */} - {isLoggedIn && (user?.role === 'admin' || user?.role === 'ADMIN' || user?.role === 'Admin') && ( - + + + navigate('/favorites')} + > + + + + + navigate('/users')} - sx={{ mr: 1 }} + onClick={() => navigate('/cart')} + aria-label={`Shopping Cart with ${cartCount} items`} > - + + + - )} - - - navigate('/wishlist')}> - - - - - - navigate('/cart')} - sx={{ mx: 0.5 }} - > - - - - - - - {isLoggedIn ? ( - - - + + + + {getInitial()} + + + + - navigate('/profile')} + aria-label="View your profile" + > + + + + Profile + + + navigate('/orders')} + aria-label="View your orders" > - {getInitial()} - - - - - - - {user?.name} - - - {user?.email} - - - - navigate('/profile')} sx={{ py: 1.5 }}> - - - - - - navigate('/orders')} sx={{ py: 1.5 }}> - - - - - - {user?.role === 'admin' || user?.role === 'ADMIN' || user?.role === 'Admin' ? ( - navigate('/admin')} sx={{ py: 1.5 }}> - + - + Orders - ) : null} - navigate('/settings')} sx={{ py: 1.5 }}> - - - - - - - - - - - - - - - ) : ( - - {!isMobile && ( - <> - - - - )} - {isMobile && ( - navigate('/login')} - > - - - )} - - )} - - - - + + + + Logout + + + + ) : ( + + )} + + + )} + + - {isMobile && renderMobileDrawer()} + {/* Mobile drawer */} + {renderMobileDrawer()} - {/* Optional secondary navbar for categories on desktop */} - {!isMobile && ( - - - - {categories.map((category) => ( - - ))} + {/* Add keyboard shortcuts help dialog */} + + + Keyboard Shortcuts: +
  • / - Focus search
  • +
  • Ctrl+H - Home page
  • +
  • Ctrl+C - Cart page
  • -
    -
    - )} -
    + } + arrow + placement="top-end" + > + + ? + +
    +
    + ); }; +// Add CSS for screen reader only content +const style = document.createElement('style'); +style.textContent = ` + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; + } +`; +document.head.appendChild(style); + export default AppBar; diff --git a/store-ui/src/components/Deals/Deals.tsx b/store-ui/src/components/Deals/Deals.tsx index 1231852..f08823d 100644 --- a/store-ui/src/components/Deals/Deals.tsx +++ b/store-ui/src/components/Deals/Deals.tsx @@ -9,74 +9,422 @@ import Chip from '@mui/material/Chip'; import Paper from '@mui/material/Paper'; import Link from '@mui/material/Link'; import StarIcon from '@mui/icons-material/Star'; +import CircularProgress from '@mui/material/CircularProgress'; +import Alert from '@mui/material/Alert'; import axiosClient, { productsUrl } from '../../api/config'; import { useState, useEffect } from 'react'; import { useNavigate } from "react-router-dom"; +import ImageOptimizer from '../ImageOptimizer'; +import { Button, IconButton, useTheme, useMediaQuery } from '@mui/material'; +import { addToCart } from '../../api/cart'; +import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; +import FavoriteIcon from '@mui/icons-material/Favorite'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; + +// Custom VisuallyHidden component for screen readers +const VisuallyHidden: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + + {children} + +); + +// Define proper types for deals +interface Deal { + dealId: string; + variantSku: string; + name: string; + shortDescription: string; + thumbnail: string; + price: number; + rating: number; +} + +// Fallback image URLs for when thumbnails fail to load +const FALLBACK_IMAGES = { + product: '/assets/images/deals/mobile.webp', // Use one of your existing images as fallback + placeholder: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE1MCIgdmlld0JveD0iMCAwIDIwMCAxNTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIxNTAiIGZpbGw9IiNGNUY1RjUiLz48cGF0aCBkPSJNNzUgNzVMMTI1IDc1TTEwMCA1MEwxMDAgMTAwIiBzdHJva2U9IiNDQ0MiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+' +}; + +// Helper function to get image source with fallback +const getImageSource = (thumbnail: string, dealName: string) => { + if (!thumbnail || thumbnail.trim() === '') { + return FALLBACK_IMAGES.product; + } + return thumbnail; +}; + +// Mock data for fallback when API returns empty results +const MOCK_DEALS: Deal[] = [ + { + dealId: 'mock-1', + variantSku: 'mock-sku-1', + name: 'Special Offer', + shortDescription: 'Great deals coming soon!', + thumbnail: '/assets/images/deals/mobile.webp', + price: 99.99, + rating: 4.5 + }, + { + dealId: 'mock-2', + variantSku: 'mock-sku-2', + name: 'Featured Product', + shortDescription: 'Amazing products at great prices', + thumbnail: '/assets/images/deals/oven.webp', + price: 199.99, + rating: 4.8 + }, + { + dealId: 'mock-3', + variantSku: 'mock-sku-3', + name: 'Best Seller', + shortDescription: 'Customer favorite items', + thumbnail: '/assets/images/deals/kurtha.webp', + price: 49.99, + rating: 4.3 + } +]; const Deals = () => { const navigate = useNavigate(); + const theme = useTheme(); + // Check if we're on a mobile device + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - const [deals, setDeals] = useState([]) - const [error, setError] = useState(null) + const [deals, setDeals] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [favorites, setFavorites] = React.useState>({}); const loadDeals = async () => { try { - const response = await axiosClient.get(productsUrl + 'deals') - setDeals(response.data) - setError(null) + setLoading(true); + setError(null); + const response = await axiosClient.get(productsUrl + 'deals'); + + // Handle empty response or invalid data structure + if (response.data && Array.isArray(response.data)) { + setDeals(response.data); + } else { + // If response is not an array or is null/undefined, set empty array + setDeals([]); + console.warn("Invalid deals data structure received:", response.data); + } } catch (err: any) { - setError(err) + setError(err); + setDeals([]); // Ensure deals is empty on error + console.error("Error fetching deals:", err); + } finally { + setLoading(false); } } // run on load useEffect(() => { - loadDeals() - }, []) + loadDeals(); + }, []); - return ( - - Deals of the Day - - <> - { - deals.slice(0, 5).map((deal: any) => ( - - { - navigate('product/' + deal.variantSku)} - } underline="none"> - - {deal.name} - - - - - {deal.shortDescription} - - - - - - - - $ {deal.price} - - } label={deal.rating} /> - - - - - - - + // Handle keyboard navigation for card links + const handleCardKeyPress = (event: React.KeyboardEvent, variantSku: string) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + navigate(`/product/${variantSku}`); + } + }; - )) + // Retry loading if there was an error + const handleRetry = () => { + loadDeals(); + }; + + const handleAddToCart = (product: any) => { + const item = { + productId: product?._id || product?.id || `product-${Math.random().toString(36).substr(2, 9)}`, + sku: product?.variants?.[0]?.sku || `sku-${Math.random().toString(36).substr(2, 9)}`, + title: product?.title, + quantity: 1, + price: product?.price, + currency: product?.currency || 'USD' + }; + addToCart(item); + }; + + const toggleFavorite = (id: string) => { + setFavorites(prev => ({ + ...prev, + [id]: !prev[id] + })); + }; + + if (loading) { + return ( + + + Loading deals of the day + + ); + } + + if (error) { + return ( + + + Retry + } - - - - ) -} + > + Failed to load deals. Please try again later. + + + ); + } + + // Use mock data if no deals are available + const displayDeals = deals.length > 0 ? deals : MOCK_DEALS; + + return ( + + + + Deals of the Day + + + Limited Time Offers + + + + + + {displayDeals.length === 0 ? ( + + No deals available at the moment. + + ) : ( + displayDeals.slice(0, 5).map((deal: Deal, index: number) => ( + + + {index === 1 && ( + + )} + + toggleFavorite(deal.dealId)} + sx={{ + position: 'absolute', + top: 5, + right: index === 1 ? (isMobile ? 40 : 60) : 5, + backgroundColor: 'rgba(255,255,255,0.8)', + width: isMobile ? 24 : 32, + height: isMobile ? 24 : 32, + padding: isMobile ? '4px' : '8px', + '&:hover': { + backgroundColor: 'rgba(255,255,255,0.9)', + } + }} + aria-label={favorites[deal.dealId] ? "Remove from favorites" : "Add to favorites"} + > + {favorites[deal.dealId] ? ( + + ) : ( + + )} + + + navigate(`/product/${deal.variantSku}`)} + sx={{ + cursor: 'pointer', + pt: 1, + px: 1, + pb: 0 + }} + > + + + + + + + navigate(`/product/${deal.variantSku}`)} + > + {deal.name} + + + + ${typeof deal.price === 'number' + ? deal.price.toFixed(2) + : parseFloat(String(deal.price || 0)).toFixed(2)} + + + + + + + )) + )} + + + + ); +}; -export default Deals \ No newline at end of file +export default Deals; \ No newline at end of file diff --git a/store-ui/src/components/ImageOptimizer.tsx b/store-ui/src/components/ImageOptimizer.tsx new file mode 100644 index 0000000..ea955c1 --- /dev/null +++ b/store-ui/src/components/ImageOptimizer.tsx @@ -0,0 +1,190 @@ +import React, { useState, useEffect, useRef } from 'react'; +import Box from '@mui/material/Box'; +import Skeleton from '@mui/material/Skeleton'; +import { styled } from '@mui/material/styles'; +import { Typography } from '@mui/material'; + +interface ImageOptimizerProps { + src: string; + alt: string; + width?: string | number; + height?: string | number; + sizes?: string; + priority?: boolean; + className?: string; + style?: React.CSSProperties; + objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; + quality?: number; + placeholder?: 'blur' | 'empty'; + blurDataURL?: string; + // New props for better accessibility + longDesc?: string; + isDecorative?: boolean; + role?: string; +} + +// Styled component for the image container to maintain aspect ratio +const AspectRatioBox = styled(Box)<{ ratio?: string }>(({ ratio = '1/1' }) => ({ + position: 'relative', + width: '100%', + '&::before': { + content: '""', + display: 'block', + paddingTop: `calc(100% / (${ratio.split('/')[0]} / ${ratio.split('/')[1]}))`, + } +})); + +// Styled component for the image with object-fit property +const StyledImage = styled('img')<{ $objectFit: string }>(({ $objectFit }) => ({ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + objectFit: $objectFit as any, +})); + +/** + * Simplified ImageOptimizer component that ensures images load properly + * Focuses on reliability over complex optimizations + */ +const ImageOptimizer: React.FC = ({ + src, + alt, + width = '100%', + height = 'auto', + sizes = '100vw', + priority = false, + className = '', + style = {}, + objectFit = 'cover', + quality = 80, + placeholder = 'empty', + blurDataURL, + longDesc = '', + isDecorative = false, + role = 'img' +}) => { + const [isLoaded, setIsLoaded] = useState(false); + const [hasError, setHasError] = useState(false); + const imgRef = useRef(null); + + // Handle image loading error + const handleError = () => { + console.error('Image failed to load:', src); + setHasError(true); + setIsLoaded(true); // Mark as loaded to remove skeleton + }; + + // Handle successful image load + const handleLoad = () => { + setIsLoaded(true); + }; + + // Determine appropriate alt text handling based on image purpose + const getImgProps = () => { + if (isDecorative) { + return { + alt: "", + "aria-hidden": true + }; + } else if (alt) { + return { + alt: alt, + "aria-hidden": false + }; + } else { + return { + alt: "Image", + "aria-hidden": false + }; + } + }; + + return ( + + {/* Loading skeleton - only show while image is loading */} + {!isLoaded && !hasError && ( + + )} + + {/* Error state */} + {hasError ? ( + + + Image not available + + + ) : ( + /* Actual image - always render but may be hidden by skeleton */ + + )} + + {/* Hidden description for complex images (for screen readers) */} + {longDesc && !isDecorative && ( + + {longDesc} + + )} + + ); +}; + +export default ImageOptimizer; \ No newline at end of file diff --git a/store-ui/src/components/SEO.tsx b/store-ui/src/components/SEO.tsx new file mode 100644 index 0000000..c01d65d --- /dev/null +++ b/store-ui/src/components/SEO.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { Helmet } from 'react-helmet-async'; + +interface SEOProps { + title?: string; + description?: string; + keywords?: string; + imageUrl?: string; + image?: string; // Add alias for imageUrl + url?: string; + type?: string; + schema?: object; + preconnectUrls?: string[]; + noindex?: boolean; // Add noindex prop + meta?: Array<{ name: string; content: string; }>; +} + +/** + * SEO component for dynamically setting meta tags for better search engine optimization + * Implements proper Open Graph and Twitter Card meta tags for social sharing + */ +const SEO: React.FC = ({ + title = 'E-Commerce Store - Quality Products at Great Prices', + description = 'Shop our wide selection of products. Find great deals on electronics, fashion, home goods, and more.', + keywords = 'e-commerce, online shopping, electronics, fashion, home goods', + imageUrl = '/logo.png', + image, // Destructure image prop + url = window.location.href, + type = 'website', + schema, + preconnectUrls = [ + 'https://fonts.googleapis.com', + 'https://fonts.gstatic.com', + 'https://cdn.jsdelivr.net' + ], + noindex, // Destructure noindex prop + meta, // Destructure meta prop +}) => { + // Use image prop if provided, otherwise use imageUrl + const finalImageUrl = image || imageUrl; + + // Convert relative image URL to absolute + const absoluteImageUrl = finalImageUrl.startsWith('http') + ? finalImageUrl + : `${window.location.origin}${finalImageUrl}`; + + // Format the JSON-LD schema data + const schemaData = schema + ? JSON.stringify(schema) + : JSON.stringify({ + "@context": "https://schema.org", + "@type": "WebSite", + "name": title, + "description": description, + "url": url, + }); + + return ( + + {/* Basic meta tags */} + {title} + + + + + {/* Preconnect for external resources to improve performance */} + {preconnectUrls.map((url, index) => ( + + ))} + + {/* Font display optimization */} + + + {/* Open Graph meta tags for social sharing (Facebook, LinkedIn) */} + + + + + + + + {/* Twitter Card meta tags */} + + + + + + {/* JSON-LD structured data */} + + + {/* Additional meta tags from meta prop */} + {meta && meta.map((m, index) => ( + + ))} + + {/* Noindex tag */} + {noindex && } + + ); +}; + +export default SEO; \ No newline at end of file diff --git a/store-ui/src/components/SearchComponent.tsx b/store-ui/src/components/SearchComponent.tsx index 0b1660c..2d541ab 100644 --- a/store-ui/src/components/SearchComponent.tsx +++ b/store-ui/src/components/SearchComponent.tsx @@ -1,302 +1,329 @@ -import React, { useState, useEffect } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useSearchParams, useNavigate } from 'react-router-dom'; import { searchUrl } from '../api/config'; +import { + Box, + TextField, + Button, + Typography, + Paper, + InputAdornment, + Chip, + CircularProgress, + Grid, + Card, + CardMedia, + CardContent, + CardActionArea, + Divider, + useTheme, + List, + ListItem, + ListItemText, + InputBase, + IconButton, + styled, + alpha +} from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; +import TuneIcon from '@mui/icons-material/Tune'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import HistoryIcon from '@mui/icons-material/History'; +import TrendingUpIcon from '@mui/icons-material/TrendingUp'; +import LocalFireDepartmentIcon from '@mui/icons-material/LocalFireDepartment'; +import CategoryIcon from '@mui/icons-material/Category'; +import BookmarkIcon from '@mui/icons-material/Bookmark'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import CloseIcon from '@mui/icons-material/Close'; +import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; + +// Styled search input similar to the screenshot +const SearchInput = styled(InputBase)(({ theme }) => ({ + width: '100%', + fontSize: '0.95rem', + padding: '8px 16px', + transition: theme.transitions.create('width'), + backgroundColor: theme.palette.mode === 'dark' ? alpha(theme.palette.common.white, 0.1) : '#f5f5f5', + borderRadius: 4, + '&:hover': { + backgroundColor: theme.palette.mode === 'dark' ? alpha(theme.palette.common.white, 0.15) : '#e5e5e5', + }, + '&.Mui-focused': { + backgroundColor: theme.palette.mode === 'dark' ? alpha(theme.palette.common.white, 0.15) : '#e5e5e5', + } +})); + +const SearchResults = styled(Paper)(({ theme }) => ({ + position: 'absolute', + width: '100%', + maxHeight: '400px', + overflow: 'auto', + zIndex: 1300, + marginTop: '4px', + borderRadius: 4, + boxShadow: '0 4px 20px rgba(0,0,0,0.15)' +})); + +const ProductCard = styled(Card)(({ theme }) => ({ + display: 'flex', + borderRadius: 4, + boxShadow: 'none', + border: `1px solid ${alpha(theme.palette.divider, 0.5)}`, + '&:hover': { + boxShadow: '0 2px 8px rgba(0,0,0,0.1)', + } +})); interface SearchResult { id: string; title: string; - description: string; - category: string; + description?: string; price: number; - imageUrl?: string; - inStock: boolean; - rating: number; - score: number; - highlights?: any; -} - -interface SearchResponse { - total: number; - products: SearchResult[]; - query: { - searchTerm: string; - category?: string; - priceRange?: { min?: string; max?: string }; - pagination: { limit: number; offset: number }; - }; + thumbnail?: string; } const SearchComponent: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams(); - const [searchTerm, setSearchTerm] = useState(searchParams.get('query') || ''); - const [results, setResults] = useState(null); - const [suggestions, setSuggestions] = useState([]); - const [loading, setLoading] = useState(false); - const [category, setCategory] = useState(searchParams.get('category') || ''); - const [minPrice, setMinPrice] = useState(searchParams.get('minPrice') || ''); - const [maxPrice, setMaxPrice] = useState(searchParams.get('maxPrice') || ''); + const [query, setQuery] = useState(searchParams.get('query') || ''); + const [results, setResults] = useState([]); + const [suggestions, setSuggestions] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [showDropdown, setShowDropdown] = useState(false); + const navigate = useNavigate(); + const inputRef = useRef(null); + const theme = useTheme(); + + // Debounce search suggestions + const debounceTimeout = useRef(null); + + const handleSearch = (event: React.FormEvent) => { + event.preventDefault(); + if (query.trim()) { + navigate(`/search?query=${encodeURIComponent(query.trim())}`); + setShowDropdown(false); + } + }; - // Search function - const handleSearch = async (queryOverride?: string) => { - const query = queryOverride || searchTerm; - if (!query.trim()) return; + const fetchSuggestions = useCallback(async (searchTerm: string) => { + if (!searchTerm || searchTerm.length < 2) { + setSuggestions([]); + return; + } - setLoading(true); try { - const params = new URLSearchParams({ - query: query, - limit: '10', - ...(category && { category }), - ...(minPrice && { minPrice }), - ...(maxPrice && { maxPrice }) - }); - - // Update URL params - setSearchParams(params); - - console.log('Making search API call to:', `${searchUrl}api/search?${params}`); - - const response = await fetch(`${searchUrl}api/search?${params}`); - const data = await response.json(); + setLoading(true); + const response = await fetch(`${searchUrl}?query=${encodeURIComponent(searchTerm)}&limit=5`); - console.log('Search API response:', response.status, data); - - if (response.ok) { - setResults(data); - } else { - console.error('Search error:', data); + if (!response.ok) { + throw new Error('Search failed'); } - } catch (error) { - console.error('Search failed:', error); - } finally { + + const data = await response.json(); + // Extract unique terms from results + const terms = data.results.map((item: any) => item.title).slice(0, 5); + setSuggestions(terms); + setLoading(false); + setError(null); + } catch (err) { + console.error('Error fetching suggestions:', err); + setError('Failed to get suggestions'); setLoading(false); } - }; - - // Handle button click - const handleSearchClick = () => { - handleSearch(); - }; + }, []); - // Auto-search when component mounts with query param - useEffect(() => { - const queryFromUrl = searchParams.get('query'); - if (queryFromUrl) { - setSearchTerm(queryFromUrl); - handleSearch(queryFromUrl); + const debounceFetchSuggestions = (searchTerm: string) => { + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); } - }, []); + + debounceTimeout.current = setTimeout(() => { + fetchSuggestions(searchTerm); + }, 300); + }; - // Get suggestions - const getSuggestions = async (query: string) => { - if (query.length < 2) { + const handleInputChange = (event: React.ChangeEvent) => { + const value = event.target.value; + setQuery(value); + + if (value.trim().length > 1) { + setShowDropdown(true); + debounceFetchSuggestions(value); + } else { + setShowDropdown(false); setSuggestions([]); - return; } + }; - try { - console.log('Making suggestions API call to:', `${searchUrl}api/suggest?query=${encodeURIComponent(query)}&limit=5`); - - const response = await fetch(`${searchUrl}api/suggest?query=${encodeURIComponent(query)}&limit=5`); - const data = await response.json(); - - console.log('Suggestions API response:', response.status, data); - - if (response.ok) { - setSuggestions(data.suggestions || []); - } - } catch (error) { - console.error('Suggestions failed:', error); + const handleSuggestionClick = (suggestion: string) => { + setQuery(suggestion); + navigate(`/search?query=${encodeURIComponent(suggestion)}`); + setShowDropdown(false); + }; + + const handleFocus = () => { + if (query.trim().length > 1) { + setShowDropdown(true); } }; - // Handle input change with debounced suggestions - useEffect(() => { - const timer = setTimeout(() => { - getSuggestions(searchTerm); - }, 300); + const handleBlur = () => { + // Delay hiding dropdown to allow clicking suggestions + setTimeout(() => { + setShowDropdown(false); + }, 200); + }; - return () => clearTimeout(timer); - }, [searchTerm]); + const handleKeyDown = (event: React.KeyboardEvent) => { + // Handle keyboard navigation in dropdown + if (!showDropdown) return; + + switch (event.key) { + case 'Escape': + setShowDropdown(false); + break; + case 'ArrowDown': + // Navigate down in suggestions + // Implementation for keyboard navigation would go here + break; + case 'ArrowUp': + // Navigate up in suggestions + break; + case 'Enter': + // Submit form will be handled by the form's onSubmit + break; + } + }; - // Debug: Show current searchUrl - useEffect(() => { - console.log('Search service URL configured as:', searchUrl); - }, []); + // Popular search terms + const popularSearches = [ + 'sneakers', 'running shoes', 'women\'s flats', 'shoe rack', 'shoe polish' + ]; return ( -
    -

    ๐Ÿ” Product Search

    - - {/* Search Form */} -
    -
    -
    - setSearchTerm(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && handleSearch()} - placeholder="Search products..." - style={{ - width: '100%', - padding: '10px', - border: '1px solid #ddd', - borderRadius: '4px', - fontSize: '16px' - }} - /> - - {/* Suggestions Dropdown */} - {suggestions.length > 0 && ( -
    - {suggestions.map((suggestion, index) => ( -
    { - setSearchTerm(suggestion.title); - setSuggestions([]); - }} - style={{ - padding: '10px', - cursor: 'pointer', - borderBottom: '1px solid #eee' - }} - onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f5f5f5'} - onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'white'} + +
    + + + + + } + endAdornment={ + query && ( + + setQuery('')} + edge="end" + aria-label="Clear search" > -
    {suggestion.title}
    -
    - {suggestion.category} - ${suggestion.price} -
    -
    - ))} -
    - )} -
    - - -
    + Search + + + - {/* Filters */} -
    - - - setMinPrice(e.target.value)} - placeholder="Min Price" - style={{ padding: '5px', border: '1px solid #ddd', borderRadius: '4px', width: '100px' }} - /> - - setMaxPrice(e.target.value)} - placeholder="Max Price" - style={{ padding: '5px', border: '1px solid #ddd', borderRadius: '4px', width: '100px' }} - /> -
    -
    - - {/* Results */} - {results && ( -
    -

    Search Results ({results.total} found)

    - - {results.products.length === 0 ? ( -

    No products found for "{results.query.searchTerm}"

    - ) : ( -
    - {results.products.map((product) => ( -
    + {loading ? ( + + + + ) : error ? ( + + {error} + + ) : suggestions.length > 0 ? ( + + {suggestions.map((suggestion, index) => ( + handleSuggestionClick(suggestion)} + role="option" + aria-selected={false} + divider > -

    - {product.highlights?.title ? ( - - ) : ( - product.title - )} -

    - -

    - {product.highlights?.description ? ( - - ) : ( - product.description - )} -

    - -
    - - ${product.price} - - - {product.inStock ? 'In Stock' : 'Out of Stock'} - -
    - -
    - Category: {product.category} | Rating: โญ {product.rating} | Score: {product.score?.toFixed(2)} -
    -
    + + + + ))} -
    + + ) : query.length > 1 ? ( + + + No suggestions found for "{query}" + + + ) : ( + + + Popular Searches + + + {popularSearches.map((term, index) => ( + handleSuggestionClick(term)} + sx={{ + backgroundColor: theme.palette.mode === 'dark' ? alpha(theme.palette.primary.main, 0.2) : alpha(theme.palette.primary.main, 0.1), + color: theme.palette.mode === 'dark' ? theme.palette.primary.light : theme.palette.primary.main, + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.palette.mode === 'dark' ? alpha(theme.palette.primary.main, 0.3) : alpha(theme.palette.primary.main, 0.2), + } + }} + /> + ))} + + )} -
    + )} -
    +
    ); }; diff --git a/store-ui/src/components/layout/CartContext.tsx b/store-ui/src/components/layout/CartContext.tsx index 3c087cb..6b73759 100644 --- a/store-ui/src/components/layout/CartContext.tsx +++ b/store-ui/src/components/layout/CartContext.tsx @@ -1,49 +1,215 @@ -import React from 'react'; -import { getCart } from '../../api/cart'; -import { useAuth } from '../../contexts/AuthContext'; +import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react'; +import { getCart, updateQuantity, removeFromCart } from '../../api/cart'; + +interface CartItem { + productId: string; + sku: string; + title: string; + quantity: number; + price: number; + currency: string; + thumbnail?: string; +} interface CartContextType { + cart: { items: CartItem[] } | null; cartCount: number; - updateCartCount: () => void; + cartTotal: number; + loading: boolean; + error: string | null; + addToCart: (item: CartItem) => Promise; + updateItemQuantity: (sku: string, quantity: number) => Promise; + removeItem: (sku: string) => Promise; + refreshCart: () => Promise; + clearError: () => void; } -const CartContext = React.createContext({ - cartCount: 0, - updateCartCount: () => {} -}); - -export const CartProvider = ({ children }: { children: React.ReactNode }) => { - const [cartCount, setCartCount] = React.useState(0); - const { isLoggedIn, user } = useAuth(); - - const updateCartCount = React.useCallback(() => { - // Only fetch cart if user is logged in - if (isLoggedIn && user) { - getCart().then((cart) => { - const count = cart?.items?.reduce((total: number, item: any) => total + item.quantity, 0) || 0; - setCartCount(count); - }).catch((error) => { - console.error('Failed to update cart count:', error); - setCartCount(0); +const CartContext = createContext(undefined); + +export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [cart, setCart] = useState<{ items: CartItem[] } | null>(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Memoized calculations for performance + const cartCount = useMemo(() => { + return cart?.items?.reduce((total, item) => total + item.quantity, 0) || 0; + }, [cart?.items]); + + const cartTotal = useMemo(() => { + return cart?.items?.reduce((total, item) => total + (item.price * item.quantity), 0) || 0; + }, [cart?.items]); + + // Clear error function + const clearError = useCallback(() => { + setError(null); + }, []); + + // Refresh cart data + const refreshCart = useCallback(async () => { + try { + setLoading(true); + setError(null); + const cartData = await getCart(); + setCart(cartData || { items: [] }); + } catch (err: any) { + setError(err.message || 'Failed to load cart'); + console.error('Error loading cart:', err); + } finally { + setLoading(false); + } + }, []); + + // Add item to cart with optimistic updates + const addToCart = useCallback(async (item: CartItem) => { + try { + setError(null); + + // Optimistic update - add item immediately to UI + setCart(prevCart => { + if (!prevCart) return { items: [item] }; + + const existingItemIndex = prevCart.items.findIndex(cartItem => cartItem.sku === item.sku); + + if (existingItemIndex >= 0) { + // Update existing item quantity + const updatedItems = [...prevCart.items]; + updatedItems[existingItemIndex] = { + ...updatedItems[existingItemIndex], + quantity: updatedItems[existingItemIndex].quantity + item.quantity + }; + return { items: updatedItems }; + } else { + // Add new item + return { items: [...prevCart.items, item] }; + } }); - } else { - // Reset cart count if not logged in - setCartCount(0); + + // Make API call + await updateQuantity(item.sku, item.quantity); + + // Refresh cart to get server state + await refreshCart(); + } catch (err: any) { + setError(err.message || 'Failed to add item to cart'); + // Revert optimistic update on error + await refreshCart(); + throw err; } - }, [isLoggedIn, user]); + }, [refreshCart]); - // Update cart count when auth state changes - React.useEffect(() => { - updateCartCount(); - }, [updateCartCount, isLoggedIn, user]); + // Update item quantity with optimistic updates + const updateItemQuantity = useCallback(async (sku: string, quantity: number) => { + const previousCartState = cart; // Declare at the beginning + + try { + setError(null); + + // Optimistic update + setCart(prevCart => { + if (!prevCart) return null; + + const updatedItems = prevCart.items.map(item => + item.sku === sku ? { ...item, quantity } : item + ).filter(item => item.quantity > 0); // Remove items with 0 quantity + + return { items: updatedItems }; + }); + + // Make API call + await updateQuantity(sku, quantity); + + // Refresh cart to get server state + await refreshCart(); + } catch (err: any) { + setError(err.message || 'Failed to update item quantity'); + // Revert optimistic update on error + setCart(previousCartState); + throw err; + } + }, [cart, refreshCart]); + + // Remove item from cart with optimistic updates + const removeItem = useCallback(async (sku: string) => { + const previousCartState = cart; // Declare at the beginning + + try { + setError(null); + + // Optimistic update + setCart(prevCart => { + if (!prevCart) return null; + + const updatedItems = prevCart.items.filter(item => item.sku !== sku); + return { items: updatedItems }; + }); + + // Make API call + await removeFromCart(sku); + + // Refresh cart to get server state + await refreshCart(); + } catch (err: any) { + setError(err.message || 'Failed to remove item from cart'); + // Revert optimistic update on error + setCart(previousCartState); + throw err; + } + }, [cart, refreshCart]); + + // Load cart on mount + useEffect(() => { + refreshCart(); + }, [refreshCart]); + + // Persist cart state to localStorage for offline resilience + useEffect(() => { + if (cart && cart.items.length > 0) { + try { + localStorage.setItem('cart_backup', JSON.stringify(cart)); + } catch (err) { + console.error('Failed to backup cart to localStorage:', err); + } + } + }, [cart]); + + // Load cart from localStorage on mount if available + useEffect(() => { + try { + const backupCart = localStorage.getItem('cart_backup'); + if (backupCart && !cart) { + const parsedCart = JSON.parse(backupCart); + setCart(parsedCart); + } + } catch (err) { + console.error('Failed to load cart backup from localStorage:', err); + } + }, []); + + const value = useMemo(() => ({ + cart, + cartCount, + cartTotal, + loading, + error, + addToCart, + updateItemQuantity, + removeItem, + refreshCart, + clearError + }), [cart, cartCount, cartTotal, loading, error, addToCart, updateItemQuantity, removeItem, refreshCart, clearError]); return ( - + {children} ); }; -export const useCart = () => React.useContext(CartContext); - -export default CartContext; +export const useCart = (): CartContextType => { + const context = useContext(CartContext); + if (!context) { + throw new Error('useCart must be used within a CartProvider'); + } + return context; +}; diff --git a/store-ui/src/components/layout/Header/Header.tsx b/store-ui/src/components/layout/Header/Header.tsx index 2a1fdf7..23c823b 100644 --- a/store-ui/src/components/layout/Header/Header.tsx +++ b/store-ui/src/components/layout/Header/Header.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import MuiAppBar from '@mui/material/AppBar'; import Box from '@mui/material/Box'; import Toolbar from '@mui/material/Toolbar'; @@ -7,7 +7,7 @@ import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import Badge from '@mui/material/Badge'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { useCart } from '../CartContext'; import DarkModeIcon from '@mui/icons-material/DarkMode'; import LightModeIcon from '@mui/icons-material/LightMode'; @@ -60,8 +60,61 @@ const StyledInputBase = styled(InputBase)(({ theme }) => ({ }, })); +// Styled skip link that properly handles focus state +const SkipLink = styled('a')(({ theme }) => ({ + position: 'absolute', + left: '-9999px', + top: 'auto', + width: '1px', + height: '1px', + overflow: 'hidden', + '&:focus': { + position: 'fixed', + top: '0', + left: '0', + width: 'auto', + height: 'auto', + padding: '12px', + backgroundColor: theme.palette.primary.main, + color: '#fff', + zIndex: 9999, + textDecoration: 'none', + fontWeight: 'bold', + borderRadius: '0 0 4px 0', + } +})); + +// Styled enhanced cart badge wrapper with improved touch target +const EnhancedCartBadge = styled('div')(({ theme }) => ({ + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + // Larger touch target area (44px is accessibility recommendation) + minWidth: '44px', + minHeight: '44px', + borderRadius: '50%', + transition: 'background-color 0.2s', + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + }, + // Pulse animation when cart has items + '@keyframes pulse': { + '0%': { + boxShadow: '0 0 0 0 rgba(255, 255, 255, 0.2)', + }, + '70%': { + boxShadow: '0 0 0 8px rgba(255, 255, 255, 0)', + }, + '100%': { + boxShadow: '0 0 0 0 rgba(255, 255, 255, 0)', + }, + }, +})); + const Header = () => { const navigate = useNavigate(); + const location = useLocation(); const { cartCount } = useCart(); const theme = useTheme(); const colorMode = React.useContext(ThemeContext); @@ -69,6 +122,9 @@ const Header = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [currentTab, setCurrentTab] = useState(0); const [searchQuery, setSearchQuery] = useState(''); + const searchInputRef = useRef(null); + const menuButtonRef = useRef(null); + const firstFocusableElementRef = useRef(null); const categories = [ { name: 'Top Offers', path: '/' }, @@ -81,6 +137,61 @@ const Header = () => { { name: 'Toys', path: '/category/toys' } ]; + // Set active tab based on current route + useEffect(() => { + const currentPath = location.pathname; + + // Find the matching category index + const activeTabIndex = categories.findIndex(category => + // Check for exact match or if we're on the home page + category.path === currentPath || + // Special case for home page + (category.path === '/' && currentPath === '/') + ); + + // If found, set it as current tab + if (activeTabIndex !== -1) { + setCurrentTab(activeTabIndex); + } else { + // If on a product page or other page, try to match the category from URL + const categoryMatch = categories.findIndex(category => + currentPath.includes(category.path) && category.path !== '/' + ); + + if (categoryMatch !== -1) { + setCurrentTab(categoryMatch); + } else { + // Default to home tab if no match + setCurrentTab(0); + } + } + }, [location.pathname]); + + // Store tab state in session storage + useEffect(() => { + try { + sessionStorage.setItem('lastActiveTab', currentTab.toString()); + } catch (error) { + console.error('Failed to save tab state:', error); + } + }, [currentTab]); + + // Load tab state from session storage on initial render + useEffect(() => { + try { + const savedTab = sessionStorage.getItem('lastActiveTab'); + if (savedTab !== null) { + const tabIndex = parseInt(savedTab, 10); + // Only set if it's a valid index and not already set by the route matching + if (!isNaN(tabIndex) && tabIndex >= 0 && tabIndex < categories.length) { + setCurrentTab(tabIndex); + } + } + } catch (error) { + console.error('Failed to load tab state:', error); + } + }, []); + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { setCurrentTab(newValue); navigate(categories[newValue].path); @@ -90,6 +201,23 @@ const Header = () => { setMobileMenuOpen(!mobileMenuOpen); }; + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setMobileMenuOpen(false); + } + }; + + // Focus management for mobile drawer + useEffect(() => { + if (mobileMenuOpen && firstFocusableElementRef.current) { + // Focus first element when drawer opens + firstFocusableElementRef.current.focus(); + } else if (!mobileMenuOpen && menuButtonRef.current) { + // Return focus to menu button when drawer closes + menuButtonRef.current.focus(); + } + }, [mobileMenuOpen]); + const handleSearch = (event: React.KeyboardEvent) => { if (event.key === 'Enter' && searchQuery.trim()) { navigate(`/search?query=${encodeURIComponent(searchQuery.trim())}`); @@ -102,11 +230,15 @@ const Header = () => { return ( + {/* Skip to content link for accessibility */} + + Skip to main content + @@ -114,10 +246,13 @@ const Header = () => { {isMobile && ( @@ -136,6 +271,10 @@ const Header = () => { marginRight: 2 }} onClick={() => navigate('/')} + tabIndex={0} + role="link" + aria-label="Home page" + onKeyDown={(e: React.KeyboardEvent) => e.key === 'Enter' && navigate('/')} > E-Commerce Store @@ -147,10 +286,11 @@ const Header = () => { )} @@ -162,28 +302,72 @@ const Header = () => { color="inherit" onClick={colorMode.toggleColorMode} sx={{ ml: 1 }} - aria-label="toggle dark mode" + aria-label={theme.palette.mode === 'dark' ? "Switch to light mode" : "Switch to dark mode"} > {theme.palette.mode === 'dark' ? : } - navigate('/cart')} - sx={{ ml: 1 }} - aria-label="shopping cart" - > - - - - + {/* Enhanced Cart Badge */} + + navigate('/cart')} + aria-label={`Shopping cart with ${cartCount} ${cartCount === 1 ? 'item' : 'items'}`} + sx={{ + position: 'relative', + animation: cartCount > 0 ? 'pulse 2s infinite' : 'none', + minWidth: '44px', + minHeight: '44px', + }} + > + + + + + @@ -191,7 +375,7 @@ const Header = () => { {!isMobile && ( - + {/* Darker grey-blue instead of bright blue */} { scrollButtons="auto" textColor="inherit" indicatorColor="secondary" - aria-label="category navigation" + aria-label="Category navigation tabs" sx={{ '& .MuiTab-root': { minWidth: 100, fontSize: '0.875rem', fontWeight: 500, + color: '#FFFFFF', + '&:hover': { + backgroundColor: 'rgba(255, 152, 0, 0.1)', + }, + '&.Mui-selected': { + color: '#FF9800', + } }, '& .MuiTabs-indicator': { - backgroundColor: '#ffffff', + backgroundColor: '#FF9800', // Orange indicator instead of white + height: '3px' + }, + '& .MuiTabScrollButton-root': { + color: '#FFFFFF', } }} > {categories.map((category, index) => ( - + ))} @@ -221,42 +420,104 @@ const Header = () => { )} - {/* Mobile Navigation Drawer */} + {/* Mobile Navigation Drawer with improved accessibility */} setMobileMenuOpen(false)} + role="dialog" + aria-modal="true" + aria-label="Main navigation menu" + onKeyDown={handleKeyDown} + id="mobile-navigation-drawer" + ModalProps={{ + // Trap focus within the drawer + disableEnforceFocus: false, + // Return focus to menu button when closed + onClose: () => { + setMobileMenuOpen(false); + if (menuButtonRef.current) { + menuButtonRef.current.focus(); + } + } + }} > - + - + + {/* Hidden help text for screen readers */} + + Press Enter to search for products + {categories.map((category, index) => ( - navigate(category.path)}> - + + ))} +
    + {/* Main content will be here */} +
    ); } diff --git a/store-ui/src/components/layout/Layout.tsx b/store-ui/src/components/layout/Layout.tsx index 8cd22e1..27eb935 100644 --- a/store-ui/src/components/layout/Layout.tsx +++ b/store-ui/src/components/layout/Layout.tsx @@ -1,60 +1,250 @@ -import React from "react" +import React, { useContext } from "react" import Header from "./Header/Header" import Footer from "./Footer/Footer" import Box from '@mui/material/Box'; -import { ThemeProvider, createTheme } from '@mui/material/styles'; -import ThemeContext from "./ThemeContext"; +import { ThemeProvider as MuiThemeProvider, createTheme } from '@mui/material/styles'; +import { useMediaQuery } from '@mui/material'; +import ThemeContext, { ThemeProvider } from "./ThemeContext"; import GlobalContext from "./GlobalContext"; import { CartProvider } from "./CartContext"; +import CssBaseline from '@mui/material/CssBaseline'; -const Layout = (props: any) => { - const [mode, setMode] = React.useState<'light' | 'dark'>('light'); +// Skip link component for keyboard accessibility +const SkipLink = () => { + return ( + { + e.currentTarget.style.top = '0'; + }} + onBlur={(e) => { + e.currentTarget.style.top = '-40px'; + }} + > + Skip to main content + + ); +}; +const Layout = (props: any) => { const [data, setData] = React.useState({}); const value = React.useMemo( () => ({ data, setData }), [data] ); - const colorMode = React.useMemo( - () => ({ - toggleColorMode: () => { - setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light')); - }, - }), - [], + return ( + + + + + ); +}; + +// Separate component to consume the theme context +const LayoutContent = (props: any) => { + const themeContext = useContext(ThemeContext); + + // Detect system preference for dark mode + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + + // Use system preference if mode hasn't been set by user + React.useEffect(() => { + if (themeContext.mode === 'light' && prefersDarkMode) { + // Only auto-switch to dark if user hasn't explicitly set light mode + const userPreference = localStorage.getItem('themeMode'); + if (!userPreference) { + themeContext.toggleColorMode(); + } + } + }, [prefersDarkMode, themeContext]); const theme = React.useMemo( - () => - createTheme({ - palette: { - mode, + () => createTheme({ + palette: { + mode: themeContext.mode, + primary: { + main: '#FF9800', // Orange from screenshot + light: '#FFB74D', + dark: '#F57C00', + contrastText: '#FFFFFF', + }, + secondary: { + main: '#263238', // Dark blue/gray from screenshot header + light: '#4F5B62', + dark: '#000A12', + contrastText: '#FFFFFF', + }, + background: { + default: themeContext.mode === 'dark' ? '#121212' : '#F5F5F5', + paper: themeContext.mode === 'dark' ? '#1E1E1E' : '#FFFFFF', + }, + text: { + primary: themeContext.mode === 'dark' ? '#FFFFFF' : '#212121', + secondary: themeContext.mode === 'dark' ? '#B0BEC5' : '#757575', + }, + divider: themeContext.mode === 'dark' ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)', + error: { + main: '#F44336', // Error red + }, + warning: { + main: '#FFC107', // Warning yellow + }, + info: { + main: '#2196F3', // Info blue + }, + success: { + main: '#4CAF50', // Success green + }, + }, + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + scrollbarColor: themeContext.mode === 'dark' ? "#6b6b6b #2b2b2b" : "#959595 #f5f5f5", + "&::-webkit-scrollbar, & *::-webkit-scrollbar": { + backgroundColor: themeContext.mode === 'dark' ? "#2b2b2b" : "#f5f5f5", + width: '8px', + }, + "&::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb": { + borderRadius: 4, + backgroundColor: themeContext.mode === 'dark' ? "#6b6b6b" : "#959595", + minHeight: 24, + } + }, + }, + }, + MuiAppBar: { + styleOverrides: { + root: { + backgroundColor: '#263238', // Dark color from screenshot header + color: '#ffffff', + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + borderRadius: '4px', + textTransform: 'none', + fontWeight: 500, + }, + contained: { + boxShadow: 'none', + '&:hover': { + boxShadow: '0 2px 4px rgba(0,0,0,0.2)', + }, + }, + containedPrimary: { + backgroundColor: '#FF9800', // Orange button from screenshot + color: '#FFFFFF', + '&:hover': { + backgroundColor: '#F57C00', + }, + }, + outlined: { + borderColor: themeContext.mode === 'dark' ? 'rgba(255,255,255,0.23)' : 'rgba(0,0,0,0.23)', + }, + }, }, - }), - [mode], + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + elevation1: { + boxShadow: '0 1px 3px rgba(0,0,0,0.12)', + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + '& .MuiOutlinedInput-root': { + borderRadius: '4px', + }, + }, + }, + }, + }, + typography: { + fontFamily: [ + 'Roboto', + '"Helvetica Neue"', + 'Arial', + 'sans-serif', + ].join(','), + h1: { + fontWeight: 500, + }, + h2: { + fontWeight: 500, + }, + h3: { + fontWeight: 500, + }, + h4: { + fontWeight: 500, + }, + h5: { + fontWeight: 500, + }, + h6: { + fontWeight: 500, + }, + button: { + fontWeight: 500, + }, + }, + shape: { + borderRadius: 4, + }, + }), + [themeContext.mode] ); return ( - - - - -
    - - {props.children} - -