diff --git a/.DS_Store b/.DS_Store
index bb29be06..1cf57d2e 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
index d6c36b79..548552a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,5 @@ Mzansi_Mail/
.venv
google-chrome-stable_current_amd64.deb
.env
-Frontend/android/app/.cxx/
\ No newline at end of file
+Frontend/android/app/.cxx/
+.DS_Store
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e21f3f8c..4a7c8618 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -11,6 +11,17 @@
"type": "dart",
"program": "lib/main_dev.dart"
},
+ {
+ "name": "Debug (web)",
+ "cwd": "mih_ui",
+ "request": "launch",
+ "type": "dart",
+ "program": "lib/main_dev.dart",
+ "args": [
+ "--web-port",
+ "1995"
+ ]
+ },
{
"name": "Profile",
"cwd": "mih_ui",
diff --git a/docker-compose.yml b/docker-compose.yml
index 5bc17734..3f209328 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,37 +4,6 @@ networks:
driver: bridge
#============== MIH Containers ====================================================================
services:
- #============== Nginx Proxy Server Old ====================================================================
- # nginx:
- # container_name: nginx
- # restart: unless-stopped
- # image: nginx
- # ports:
- # - 80:80
- # - 443:443
- # volumes:
- # - ./nginx/nginx.conf:/etc/nginx/nginx.conf
- # - certbotConf:/etc/letsencrypt
- # - certbotChall:/var/www/certbot
- # depends_on:
- # - mih-ux
- # networks:
- # - mih-network
- # profiles: [ 'prod' ]
- #============== Cert Bot Old ====================================================================
- # certbot:
- # image: certbot/certbot
- # container_name: certbot
- # volumes:
- # - certbotConf:/etc/letsencrypt
- # - certbotChall:/var/www/certbot
- # #command: certonly --test-cert --webroot -w /var/www/certbot --force-renewal --email yasienmeth@gmail.com -d mzansi-innovation-hub.co.za -d www.mzansi-innovation-hub.co.za --agree-tos
- # command: certonly --webroot -w /var/www/certbot --force-renewal --email ${CERTBOT_EMAIL} -d ${CERTBOT_APP_DOMAIN} -d ${CERTBOT_API_DOMAIN} -d ${CERTBOT_STORAGE_DOMAIN} -d ${CERTBOT_MONITOR_DOMAIN} -d ${CERTBOT_AI_DOMAIN} --agree-tos
- # networks:
- # - mih-network
- # depends_on:
- # - nginx
- # profiles: [ 'withCert' ]
#============== Nginx Proxy Manager ====================================================================
mih-nginx:
container_name: mih-nginx
@@ -43,7 +12,7 @@ services:
ports:
- '80:80' # Public HTTP
- '443:443' # Public HTTPS
- - '81:81' # Admin Web Port
+ - '127.0.0.1:81:81' # Admin Web Port
volumes:
- ./mih_nginx/data:/data
- ./mih_nginx/letsencrypt:/etc/letsencrypt
@@ -69,8 +38,8 @@ services:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- - "3000:3000"
- - "222:22"
+ - "127.0.0.1:3000:3000"
+ - "127.0.0.1:222:22"
depends_on:
mih-gitea-db:
condition: service_healthy
@@ -99,7 +68,7 @@ services:
depends_on:
- mih-db
ports:
- - 3567:3567
+ - '127.0.0.1:3567:3567'
environment:
REFRESH_TOKEN_VALIDITY: '604800'
ACCESS_TOKEN_VALIDITY: '86400'
@@ -125,7 +94,7 @@ services:
image: wordpress
restart: always
ports:
- - 8081:80
+ - '127.0.0.1:8081:80'
environment:
WORDPRESS_DB_HOST: mih-wp-db
WORDPRESS_DB_USER: ${WP_SQL_USER}
@@ -154,7 +123,7 @@ services:
build:
context: ./mih_ui
ports:
- - "83:83"
+ - "127.0.0.1:83:83"
networks:
- mih-network
depends_on:
@@ -166,7 +135,7 @@ services:
target: builder
container_name: mih-api-hub
ports:
- - 8080:80
+ - "127.0.0.1:8080:80"
volumes:
- ./mih_api_hub:/app
networks:
@@ -187,7 +156,7 @@ services:
networks:
- mih-network
ports:
- - '3306:3306'
+ - '127.0.0.1:3306:3306'
volumes:
- ./mih_db:/var/lib/mysql
#============== PHP My Admin ====================================================================
@@ -213,8 +182,8 @@ services:
hostname: mih-minio
image: minio/minio
ports:
- - '9000:9000'
- - '9001:9001'
+ - '127.0.0.1:9000:9000'
+ - '127.0.0.1:9001:9001'
volumes:
- './mih_minio:/data'
environment:
@@ -228,7 +197,7 @@ services:
container_name: mih-monitor
image: portainer/portainer-ce:2.20.3
ports:
- - 9444:9443
+ - '127.0.0.1:9444:9443'
volumes:
- ./mih_monitor/data:/data
- /var/run/docker.sock:/var/run/docker.sock
@@ -240,7 +209,7 @@ services:
container_name: mih-ai
image: ollama/ollama:latest
ports:
- - 11434:11434
+ - '127.0.0.1:11434:11434'
volumes:
- ./mih_ai/ollama/ollama:/root/.ollama
pull_policy: always
diff --git a/mih_api_hub/.gitignore b/mih_api_hub/.gitignore
index 156d46ab..fb1147e5 100644
--- a/mih_api_hub/.gitignore
+++ b/mih_api_hub/.gitignore
@@ -1,3 +1,4 @@
.env
__pycache__/
-temp*.pdf
\ No newline at end of file
+temp*.pdf
+.DS_Store
\ No newline at end of file
diff --git a/mih_api_hub/__init__.py b/mih_api_hub/__init__.py
index 51d9c310..e69de29b 100644
--- a/mih_api_hub/__init__.py
+++ b/mih_api_hub/__init__.py
@@ -1,28 +0,0 @@
-# from supertokens_python import init, InputAppInfo, SupertokensConfig
-# from supertokens_python.recipe import emailpassword, session, dashboard
-
-# init(
-# app_info=InputAppInfo(
-# app_name="MIH_API_HUB",
-# api_domain="http://localhost:8080/",
-# website_domain="http://mzansi-innovation-hub.co.za",
-# api_base_path="/auth",
-# website_base_path="/auth"
-# ),
-# supertokens_config=SupertokensConfig(
-# # https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core.
-# connection_uri="supertokens:3567/",
-# api_key="leatucczyixqwkqqdrhayiwzeofkltds"
-# ),
-# framework='fastapi',
-# recipe_list=[
-# # SuperTokens.init(),
-# session.init(), # initializes session features
-# emailpassword.init(),
-# dashboard.init(admins=[
-# "yasienmeth@gmail.com",
-# ],
-# )
-# ],
-# mode='wsgi' # use wsgi instead of asgi if you are running using gunicorn
-# )
\ No newline at end of file
diff --git a/mih_api_hub/main.py b/mih_api_hub/main.py
index bb107d42..3d1151f1 100644
--- a/mih_api_hub/main.py
+++ b/mih_api_hub/main.py
@@ -34,14 +34,20 @@ from supertokens_python.recipe.session.framework.fastapi import verify_session
from supertokens_python.recipe.emailverification import EmailVerificationClaim
from supertokens_python.recipe.session import SessionContainer
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
+st_api_key = os.getenv("SUPERTOKENS_API_KEY")
origins = [
"http://localhost",
"http://localhost:80",
+ "http://localhost:1995",
"http://localhost:8080",
"http://MIH-API-Hub:80",
"http://MIH-API-Hub",
"http://api.mzansi-innovation-hub.co.za",
- "*",
+ "http://app.mzansi-innovation-hub.co.za",
]
init(
@@ -55,7 +61,7 @@ init(
supertokens_config=SupertokensConfig(
# https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core.
connection_uri="http://mih-supertokens:3567/",
- api_key="leatucczyixqwkqqdrhayiwzeofkltds"
+ api_key=st_api_key
),
framework='fastapi',
recipe_list=[
diff --git a/mih_api_hub/requirements.txt b/mih_api_hub/requirements.txt
index 1cca0eba..f3fd0d07 100644
--- a/mih_api_hub/requirements.txt
+++ b/mih_api_hub/requirements.txt
@@ -9,5 +9,5 @@ watchfiles
python-multipart
python-dotenv
xlrd
-supertokens-python
+supertokens-python==0.24.0
sniffio
\ No newline at end of file
diff --git a/mih_ui/.gitignore b/mih_ui/.gitignore
index 9fc6e4ac..5af64235 100644
--- a/mih_ui/.gitignore
+++ b/mih_ui/.gitignore
@@ -1,3 +1,5 @@
+.env
+
# Miscellaneous
*.class
*.log
diff --git a/mih_ui/Dockerfile b/mih_ui/Dockerfile
index 1664c3c1..fe4435fc 100644
--- a/mih_ui/Dockerfile
+++ b/mih_ui/Dockerfile
@@ -1,48 +1,36 @@
-# Install Operating system and dependencies
-#FROM ubuntu:22.04
+# --- STAGE 1: The Builder ---
FROM debian:latest AS build-env
-#ENV DEBIAN_FRONTEND=noninteractive
+# Install necessary dependencies for Flutter
+RUN apt-get update && apt-get install -y \
+ curl git wget unzip libglu1-mesa fonts-droid-fallback python3 \
+ && rm -rf /var/lib/apt/lists/*
-RUN apt-get update --fix-missing
-
-RUN apt-get install -y curl git wget unzip gdb libstdc++6 libglu1-mesa fonts-droid-fallback
-# RUN apt-get install -y curl git wget unzip libgconf-2-4 gdb libstdc++6 libglu1-mesa fonts-droid-fallback
-RUN apt-get install python3 -y
-
-# download Flutter SDK from Flutter Github repo
+# Clone Flutter SDK
RUN git clone -b flutter-3.32-candidate.0 https://github.com/flutter/flutter.git /usr/local/flutter
-# RUN git clone -b stable https://github.com/flutter/flutter.git /usr/local/flutter
-
-# Set flutter environment path
ENV PATH="/usr/local/flutter/bin:/usr/local/flutter/bin/cache/dart-sdk/bin:${PATH}"
-#ENV PATH "$PATH:/home/developer/flutter/bin"
-
-RUN flutter doctor -v
-
-# Enable flutter web
-RUN flutter channel flutter-3.32-candidate.0
-# RUN flutter channel stable
-RUN flutter config --enable-web
-
-# Copy files to container and build
-RUN mkdir /app/
-COPY . /app/
+# Build the Flutter web app
WORKDIR /app
-# RUN flutter upgrade
+COPY . .
+RUN flutter config --enable-web
RUN flutter build web --release -t ./lib/main_prod.dart
-# RUN flutter build web --release --web-renderer canvaskit -t ./lib/main_prod.dart
+# --- STAGE 2: The Final Production Image ---
+FROM nginx:alpine
-# RUN cd ..
+# Copy built files from the first stage
+COPY --from=build-env /app/build/web /usr/share/nginx/html
-# WORKDIR /app/build/web/
+# Create the Nginx config inside the Dockerfile to handle SPA routing
+RUN echo 'server { \
+ listen 83; \
+ location / { \
+ root /usr/share/nginx/html; \
+ index index.html; \
+ try_files $uri $uri/ /index.html; \
+ } \
+ }' > /etc/nginx/conf.d/default.conf
EXPOSE 83
-
-RUN ["chmod", "+x", "/app/server/server.sh"]
-
-ENTRYPOINT [ "/app/server/server.sh"]
-
-# RUN ["python3", "-u", "/app/server/MIH_web_server.py"]
\ No newline at end of file
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/mih_ui/android/app/src/main/res/drawable/mzansi_directory.xml b/mih_ui/android/app/src/main/res/drawable/mzansi_directory.xml
index 5cd49d32..1ee03963 100644
--- a/mih_ui/android/app/src/main/res/drawable/mzansi_directory.xml
+++ b/mih_ui/android/app/src/main/res/drawable/mzansi_directory.xml
@@ -5,7 +5,6 @@ android:height="23.53125dp"
android:width="24dp">
+ android:fillColor="@color/mih_icon_foreground"/>
diff --git a/mih_ui/android/app/src/main/res/values/arrays.xml b/mih_ui/android/app/src/main/res/values/arrays.xml
index fb766383..669016d1 100644
--- a/mih_ui/android/app/src/main/res/values/arrays.xml
+++ b/mih_ui/android/app/src/main/res/values/arrays.xml
@@ -5,5 +5,6 @@
- @drawable/mzansi_wallet_sc
- @drawable/mzansi_ai_sc
- @drawable/mih_calculator_sc
+ - @drawable/mzansi_directory_sc
\ No newline at end of file
diff --git a/mih_ui/lib/main_dev.dart b/mih_ui/lib/main_dev.dart
index 437c62b9..e1c394dd 100644
--- a/mih_ui/lib/main_dev.dart
+++ b/mih_ui/lib/main_dev.dart
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_web_plugins/url_strategy.dart'
if (dart.library.html) 'package:flutter_web_plugins/url_strategy.dart';
@@ -42,6 +43,7 @@ void main() async {
debugPrint('APP INSTALLED!');
});
final GoRouter appRouter = MihGoRouter().mihRouter;
+ await dotenv.load(fileName: ".env");
FlutterNativeSplash.remove();
runApp(MzansiInnovationHub(
router: appRouter,
diff --git a/mih_ui/lib/main_prod.dart b/mih_ui/lib/main_prod.dart
index c8ce2935..b7026b50 100644
--- a/mih_ui/lib/main_prod.dart
+++ b/mih_ui/lib/main_prod.dart
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_web_plugins/url_strategy.dart'
if (dart.library.html) 'package:flutter_web_plugins/url_strategy.dart';
@@ -33,6 +34,7 @@ void main() async {
debugPrint('APP INSTALLED!');
});
final GoRouter appRouter = MihGoRouter().mihRouter;
+ await dotenv.load(fileName: ".env");
FlutterNativeSplash.remove();
runApp(MzansiInnovationHub(
router: appRouter,
diff --git a/mih_ui/lib/mih_config/mih_env.dart b/mih_ui/lib/mih_config/mih_env.dart
index e9dcdd2f..9af57766 100644
--- a/mih_ui/lib/mih_config/mih_env.dart
+++ b/mih_ui/lib/mih_config/mih_env.dart
@@ -20,13 +20,13 @@ abstract class AppEnviroment {
switch (env) {
case Enviroment.dev:
{
- if (kIsWeb) {
+ if (kIsWeb || Platform.isIOS || Platform.isLinux) {
//================= Web Dev Urls =================
baseAppUrl = "http://localhost:80";
baseApiUrl = "http://localhost:8080";
baseFileUrl = "http://localhost:9000";
baseAiUrl = "http://localhost:11434";
- bannerAdUnitId = 'ca-app-pub-3940256099942544/2435281174';
+ bannerAdUnitId = 'ca-app-pub-3940256099942544/2435281174'; // IOS ID
break;
} else if (Platform.isAndroid) {
//================= Android Dev Urls =================
@@ -35,14 +35,6 @@ abstract class AppEnviroment {
baseFileUrl = "http://10.0.2.2:9000";
baseAiUrl = "http://10.0.2.2:11434";
bannerAdUnitId = 'ca-app-pub-3940256099942544/9214589741';
- } else {
- //================= Web & iOS Dev Urls =================
- baseAppUrl = "http://localhost:80";
- baseApiUrl = "http://localhost:8080";
- baseFileUrl = "http://localhost:9000";
- baseAiUrl = "http://localhost:11434";
- bannerAdUnitId = 'ca-app-pub-3940256099942544/2435281174';
- break;
}
}
case Enviroment.prod:
diff --git a/mih_ui/lib/mih_config/mih_theme.dart b/mih_ui/lib/mih_config/mih_theme.dart
index dd7ea4c1..1e4870cc 100644
--- a/mih_ui/lib/mih_config/mih_theme.dart
+++ b/mih_ui/lib/mih_config/mih_theme.dart
@@ -9,7 +9,7 @@ class MihTheme {
late String loadingAssetText;
late TargetPlatform platform;
bool kIsWeb = const bool.fromEnvironment('dart.library.js_util');
- String latestVersion = "1.2.5";
+ String latestVersion = "1.2.6";
MihTheme() {
mode = "Dark";
}
@@ -98,6 +98,12 @@ class MihTheme {
return "Android";
} else if (platform == TargetPlatform.iOS) {
return "iOS";
+ } else if (platform == TargetPlatform.linux) {
+ return "Linux";
+ } else if (platform == TargetPlatform.macOS) {
+ return "macOS";
+ } else if (platform == TargetPlatform.windows) {
+ return "Windows";
}
}
return "Other";
diff --git a/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_one.dart b/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_one.dart
index 3406a341..95de0348 100644
--- a/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_one.dart
+++ b/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_one.dart
@@ -1,3 +1,5 @@
+import 'dart:io';
+
import 'package:country_code_picker/country_code_picker.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
@@ -161,6 +163,7 @@ class _PackageToolOneState extends State {
return Stack(
children: [
MihSingleChildScroll(
+ scrollbarOn: true,
child: Padding(
padding:
MzansiInnovationHub.of(context)!.theme.screenType == "desktop"
@@ -450,7 +453,7 @@ class _PackageToolOneState extends State {
),
],
),
- MihBannerAd(),
+ if (Platform.isAndroid || Platform.isIOS) MihBannerAd(),
const SizedBox(height: 10),
Divider(
color: MihColors.getSecondaryColor(
@@ -773,6 +776,7 @@ class _PackageToolOneState extends State {
MihCircleAvatar(
imageFile: imagePreview,
width: 50,
+ expandable: true,
editable: false,
fileNameController: _fileNameController,
userSelectedfile: file,
diff --git a/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_three.dart b/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_three.dart
index 1682426a..c91f3299 100644
--- a/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_three.dart
+++ b/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_three.dart
@@ -111,6 +111,7 @@ class _PackageToolThreeState extends State {
return Stack(
children: [
MihSingleChildScroll(
+ scrollbarOn: true,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
diff --git a/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_two.dart b/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_two.dart
index f6713a51..181f34b5 100644
--- a/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_two.dart
+++ b/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_two.dart
@@ -24,6 +24,7 @@ class _PackageToolTwoState extends State {
Widget getBody() {
return MihSingleChildScroll(
+ scrollbarOn: true,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
diff --git a/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_zero.dart b/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_zero.dart
index 5b9cf66a..7bf24130 100644
--- a/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_zero.dart
+++ b/mih_ui/lib/mih_package_components/Example/package_tools/package_tool_zero.dart
@@ -25,6 +25,7 @@ class _PackageToolZeroState extends State {
Widget getBody() {
return MihSingleChildScroll(
+ scrollbarOn: true,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
diff --git a/mih_ui/lib/mih_package_components/mih_business_info_card_v2.dart b/mih_ui/lib/mih_package_components/mih_business_info_card_v2.dart
new file mode 100644
index 00000000..2c9684d7
--- /dev/null
+++ b/mih_ui/lib/mih_package_components/mih_business_info_card_v2.dart
@@ -0,0 +1,659 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:mzansi_innovation_hub/main.dart';
+import 'package:mzansi_innovation_hub/mih_objects/bookmarked_business.dart';
+import 'package:mzansi_innovation_hub/mih_objects/business.dart';
+import 'package:mzansi_innovation_hub/mih_objects/business_review.dart';
+import 'package:mzansi_innovation_hub/mih_package_components/mih_button.dart';
+import 'package:mzansi_innovation_hub/mih_package_components/mih_icons.dart';
+import 'package:mzansi_innovation_hub/mih_package_components/mih_package_window.dart';
+import 'package:mzansi_innovation_hub/mih_providers/mzansi_directory_provider.dart';
+import 'package:mzansi_innovation_hub/mih_config/mih_colors.dart';
+import 'package:mzansi_innovation_hub/mih_packages/mzansi_profile/business_profile/components/mih_add_bookmark_alert.dart';
+import 'package:mzansi_innovation_hub/mih_packages/mzansi_profile/business_profile/components/mih_delete_bookmark_alert.dart';
+import 'package:mzansi_innovation_hub/mih_packages/mzansi_profile/business_profile/components/mih_review_business_window.dart';
+import 'package:mzansi_innovation_hub/mih_providers/mzansi_profile_provider.dart';
+import 'package:mzansi_innovation_hub/mih_services/mih_alert_services.dart';
+import 'package:mzansi_innovation_hub/mih_services/mih_business_details_services.dart';
+import 'package:mzansi_innovation_hub/mih_services/mih_file_services.dart';
+import 'package:mzansi_innovation_hub/mih_services/mih_mzansi_directory_services.dart';
+import 'package:provider/provider.dart';
+import 'package:redacted/redacted.dart';
+import 'package:supertokens_flutter/supertokens.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+class MihBusinessCardV2 extends StatefulWidget {
+ final Business business;
+ final double width;
+ const MihBusinessCardV2({
+ super.key,
+ required this.business,
+ required this.width,
+ });
+
+ @override
+ State createState() => _MihBusinessCardV2State();
+}
+
+class _MihBusinessCardV2State extends State {
+ Future? _businessReviewFuture;
+ Future? _bookmarkedBusinessFuture;
+ bool _isUserSignedIn = false;
+
+ Future _checkUserSession() async {
+ final doesSessionExist = await SuperTokens.doesSessionExist();
+ setState(() {
+ _isUserSignedIn = doesSessionExist;
+ });
+ }
+
+ RedactedConfiguration getRedactedConfiguration() {
+ return RedactedConfiguration(
+ // redactedColor: Colors.pink,
+ redactedColor: MihColors.getPrimaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
+ );
+ }
+
+ Future _makePhoneCall(String phoneNumber) async {
+ String formattedNumber = phoneNumber.replaceAll("-", "");
+ final Uri url = Uri(scheme: 'tel', path: formattedNumber);
+ if (await canLaunchUrl(url)) {
+ await launchUrl(url);
+ } else {
+ MihAlertServices().errorBasicAlert(
+ "Error Making Call",
+ "We couldn't open your phone app to call $formattedNumber. To fix this, make sure you have a phone application installed and it's set as your default dialer.",
+ context,
+ );
+ }
+ }
+
+ String? _encodeQueryParameters(Map params) {
+ return params.entries
+ .map((MapEntry e) =>
+ '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
+ .join('&');
+ }
+
+ Future _launchEmail(
+ String recipient, String subject, String body) async {
+ final Uri emailLaunchUri = Uri(
+ scheme: 'mailto',
+ path: recipient,
+ query: _encodeQueryParameters({
+ 'subject': subject,
+ 'body': body,
+ }),
+ );
+
+ if (await canLaunchUrl(emailLaunchUri)) {
+ await launchUrl(emailLaunchUri);
+ } else {
+ MihAlertServices().errorBasicAlert(
+ "Error Creating Email",
+ "We couldn't launch your email app to send a message to $recipient. To fix this, please confirm that you have an email application installed and that it's set as your default.",
+ context,
+ );
+ }
+ }
+
+ Future _launchGoogleMapsWithUrl({
+ required double latitude,
+ required double longitude,
+ String? label,
+ }) async {
+ final Uri googleMapsUrl = Uri.parse(
+ 'https://www.google.com/maps/search/?api=1&query=$latitude,$longitude${label != null ? '&query_place_id=' : ''}',
+ );
+ try {
+ if (await canLaunchUrl(googleMapsUrl)) {
+ await launchUrl(googleMapsUrl);
+ } else {
+ MihAlertServices().errorBasicAlert(
+ "Error Opening Maps",
+ "There was an issue opening maps for ${widget.business.Name}. This usually happens if you don't have a maps app installed or it's not set as your default. Please install one to proceed.",
+ context,
+ );
+ }
+ } catch (e) {
+ MihAlertServices().errorBasicAlert(
+ "Error Opening Maps",
+ "There was an issue opening maps for ${widget.business.Name}. This usually happens if you don't have a maps app installed or it's not set as your default. Please install one to proceed.",
+ context,
+ );
+ }
+ }
+
+ Future _launchWebsite(String urlString) async {
+ String newUrl = urlString;
+ if (!newUrl.startsWith("https://")) {
+ newUrl = "https://$urlString";
+ }
+ final Uri url = Uri.parse(newUrl);
+ try {
+ if (await canLaunchUrl(url)) {
+ await launchUrl(url);
+ } else {
+ MihAlertServices().errorBasicAlert(
+ "Error Opening Website",
+ "We couldn't open the link to $newUrl. To view this website, please ensure you have a web browser installed and set as your default.",
+ context,
+ );
+ }
+ } catch (e) {
+ MihAlertServices().errorBasicAlert(
+ "Error Opening Website",
+ "We couldn't open the link to $newUrl. To view this website, please ensure you have a web browser installed and set as your default.",
+ context,
+ );
+ }
+ }
+
+ Future getUserReview() async {
+ String user_id = await SuperTokens.getUserId();
+ return await MihMzansiDirectoryServices().getUserReviewOfBusiness(
+ user_id,
+ widget.business.business_id,
+ );
+ }
+
+ Future getUserBookmark() async {
+ String user_id = await SuperTokens.getUserId();
+ return await MihMzansiDirectoryServices().getUserBookmarkOfBusiness(
+ user_id,
+ widget.business.business_id,
+ );
+ }
+
+ bool isValidGps(String coordinateString) {
+ final RegExp gpsRegex = RegExp(
+ r"^-?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*-?(1[0-7]\d(\.\d+)?|180(\.0+)?|\d{1,2}(\.\d+)?)$");
+ return gpsRegex.hasMatch(coordinateString);
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _checkUserSession();
+ _businessReviewFuture = getUserReview();
+ _bookmarkedBusinessFuture = getUserBookmark();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // double screenWidth = MediaQuery.of(context).size.width;
+ return Consumer2(
+ builder: (BuildContext context, MzansiProfileProvider profileProvider,
+ MzansiDirectoryProvider directoryProvider, Widget? child) {
+ double iconSize = 50.0;
+ return Wrap(
+ alignment: WrapAlignment.center,
+ runSpacing: 10,
+ spacing: 10,
+ children: [
+ Column(
+ children: [
+ MihButton(
+ width: 80,
+ height: 80,
+ onPressed: () {
+ _makePhoneCall(widget.business.contact_no);
+ },
+ buttonColor: MihColors.getGreenColor(
+ MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
+ child: Icon(
+ Icons.phone,
+ color: MihColors.getPrimaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
+ size: iconSize,
+ ),
+ ),
+ const SizedBox(height: 2),
+ FittedBox(
+ child: Text(
+ "Call",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: MihColors.getSecondaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ fontSize: 20,
+ ),
+ ),
+ ),
+ ],
+ ),
+ Column(
+ children: [
+ MihButton(
+ width: 80,
+ height: 80,
+ onPressed: () {
+ _launchEmail(
+ widget.business.bus_email,
+ "Inquiery about ${widget.business.Name}",
+ "Dear ${widget.business.Name},\n\nI would like to inquire about your services.\n\nBest regards,\n",
+ );
+ },
+ buttonColor: MihColors.getPinkColor(
+ MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
+ child: Icon(
+ Icons.email,
+ color: MihColors.getPrimaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
+ size: iconSize,
+ ),
+ ),
+ const SizedBox(height: 2),
+ FittedBox(
+ child: Text(
+ "Email",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: MihColors.getSecondaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ fontSize: 20,
+ ),
+ ),
+ ),
+ ],
+ ),
+ if (isValidGps(widget.business.gps_location))
+ Column(
+ children: [
+ MihButton(
+ width: 80,
+ height: 80,
+ onPressed: () {
+ final latitude = double.parse(
+ widget.business.gps_location.split(',')[0]);
+ final longitude = double.parse(
+ widget.business.gps_location.split(',')[1]);
+ _launchGoogleMapsWithUrl(
+ latitude: latitude,
+ longitude: longitude,
+ );
+ },
+ buttonColor: MihColors.getOrangeColor(
+ MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
+ child: Icon(
+ Icons.location_on,
+ color: MihColors.getPrimaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ size: iconSize,
+ ),
+ ),
+ const SizedBox(height: 2),
+ FittedBox(
+ child: Text(
+ "Maps",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: MihColors.getSecondaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ fontSize: 20,
+ ),
+ ),
+ ),
+ ],
+ ),
+ if (widget.business.website.isNotEmpty &&
+ widget.business.website != "")
+ Column(
+ children: [
+ MihButton(
+ width: 80,
+ height: 80,
+ onPressed: () {
+ _launchWebsite(widget.business.website);
+ },
+ buttonColor: MihColors.getRedColor(
+ MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
+ child: Icon(
+ Icons.language,
+ color: MihColors.getPrimaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ size: iconSize,
+ ),
+ ),
+ const SizedBox(height: 2),
+ FittedBox(
+ child: Text(
+ "Website",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: MihColors.getSecondaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ fontSize: 20,
+ ),
+ ),
+ ),
+ ],
+ ),
+ FutureBuilder(
+ future: _businessReviewFuture,
+ builder: (context, asyncSnapshot) {
+ if (asyncSnapshot.connectionState == ConnectionState.waiting) {
+ return Column(
+ children: [
+ MihButton(
+ width: 80,
+ height: 80,
+ onPressed: () {},
+ buttonColor: MihColors.getGreyColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ child: Icon(
+ Icons.star_rate_rounded,
+ color: MihColors.getPrimaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ size: iconSize,
+ ),
+ ).redacted(context: context, redact: true),
+ const SizedBox(height: 2),
+ FittedBox(
+ child: Text(
+ "Rate Us",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: MihColors.getSecondaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ fontSize: 20,
+ ),
+ ).redacted(context: context, redact: true),
+ ),
+ ],
+ );
+ } else {
+ BusinessReview? businessReview = asyncSnapshot.data;
+ String ratingTitle = "";
+ if (businessReview == null) {
+ ratingTitle = "Rate Us";
+ } else {
+ ratingTitle = "Edit";
+ }
+ return Column(
+ children: [
+ MihButton(
+ width: 80,
+ height: 80,
+ onPressed: () {
+ businessReviewRatingWindow(directoryProvider,
+ businessReview, true, widget.width);
+ },
+ buttonColor: MihColors.getYellowColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ child: Icon(
+ Icons.star_rate_rounded,
+ color: MihColors.getPrimaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ size: iconSize,
+ ),
+ ),
+ const SizedBox(height: 2),
+ FittedBox(
+ child: Text(
+ ratingTitle,
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: MihColors.getSecondaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ fontSize: 20,
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+ },
+ ),
+ FutureBuilder(
+ future: _bookmarkedBusinessFuture,
+ builder: (context, asyncSnapshot) {
+ if (asyncSnapshot.connectionState == ConnectionState.waiting) {
+ return Column(
+ children: [
+ MihButton(
+ width: 80,
+ height: 80,
+ onPressed: () {},
+ buttonColor: MihColors.getGreyColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ child: Icon(
+ Icons.bookmark_add_rounded,
+ color: MihColors.getPrimaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ size: iconSize,
+ ),
+ ).redacted(context: context, redact: true),
+ const SizedBox(height: 2),
+ FittedBox(
+ child: Text(
+ "bookmark",
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: MihColors.getSecondaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ fontSize: 20,
+ ),
+ ).redacted(context: context, redact: true),
+ ),
+ ],
+ );
+ } else {
+ BookmarkedBusiness? bookmarkBusiness = asyncSnapshot.data;
+ String bookmarkDisplayTitle = "";
+ if (bookmarkBusiness == null) {
+ bookmarkDisplayTitle = "Bookmark";
+ } else {
+ bookmarkDisplayTitle = "Remove";
+ }
+ return Column(
+ children: [
+ MihButton(
+ width: 80,
+ height: 80,
+ onPressed: () {
+ if (bookmarkBusiness == null) {
+ showAddBookmarkAlert();
+ } else {
+ showDeleteBookmarkAlert(bookmarkBusiness);
+ }
+ },
+ buttonColor: MihColors.getBluishPurpleColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ child: Icon(
+ bookmarkBusiness == null
+ ? Icons.bookmark_add_rounded
+ : Icons.bookmark_remove_rounded,
+ color: MihColors.getPrimaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ size: iconSize,
+ ),
+ ),
+ const SizedBox(height: 2),
+ FittedBox(
+ child: Text(
+ bookmarkDisplayTitle,
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: MihColors.getSecondaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ fontSize: 20,
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+ },
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ Future businessReviewRatingWindow(
+ MzansiDirectoryProvider directoryProvider,
+ BusinessReview? myReview,
+ bool previouslyRated,
+ double width) async {
+ if (_isUserSignedIn) {
+ showDialog(
+ barrierDismissible: false,
+ context: context,
+ builder: (context) => MihReviewBusinessWindow(
+ business: widget.business,
+ businessReview: myReview,
+ screenWidth: width,
+ readOnly: false,
+ onSuccessDismissPressed: () async {
+ List? businessSearchResults = [];
+ businessSearchResults = await MihBusinessDetailsServices()
+ .searchBusinesses(directoryProvider.searchTerm,
+ directoryProvider.businessTypeFilter, context);
+ Map> busImagesUrl = {};
+ Future businessLogoUrl;
+ for (var bus in businessSearchResults) {
+ businessLogoUrl = MihFileApi.getMinioFileUrl(bus.logo_path);
+ busImagesUrl[bus.business_id] = businessLogoUrl;
+ }
+ directoryProvider.setSearchedBusinesses(
+ searchedBusinesses: businessSearchResults,
+ businessesImagesUrl: busImagesUrl,
+ );
+ setState(() {
+ _businessReviewFuture = getUserReview();
+ });
+ },
+ ),
+ );
+ } else {
+ showSignInRequiredAlert();
+ }
+ }
+
+ void showAddBookmarkAlert() {
+ if (_isUserSignedIn) {
+ showDialog(
+ barrierDismissible: false,
+ context: context,
+ builder: (context) => MihAddBookmarkAlert(
+ business: widget.business,
+ onSuccessDismissPressed: () async {
+ _bookmarkedBusinessFuture = getUserBookmark();
+ },
+ ),
+ );
+ } else {
+ showSignInRequiredAlert();
+ }
+ }
+
+ void showDeleteBookmarkAlert(BookmarkedBusiness? bookmarkBusiness) {
+ if (_isUserSignedIn) {
+ showDialog(
+ barrierDismissible: false,
+ context: context,
+ builder: (context) => MihDeleteBookmarkAlert(
+ business: widget.business,
+ bookmarkBusiness: bookmarkBusiness,
+ onSuccessDismissPressed: () {
+ _bookmarkedBusinessFuture = getUserBookmark();
+ },
+ // startUpSearch: widget.startUpSearch,
+ ));
+ } else {
+ showSignInRequiredAlert();
+ }
+ }
+
+ void showSignInRequiredAlert() {
+ showDialog(
+ barrierDismissible: false,
+ context: context,
+ builder: (context) {
+ return MihPackageWindow(
+ fullscreen: false,
+ windowTitle: null,
+ onWindowTapClose: () {
+ context.pop();
+ },
+ windowBody: Column(
+ children: [
+ Icon(
+ MihIcons.mihLogo,
+ size: 125,
+ color: MihColors.getSecondaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
+ ),
+ const SizedBox(height: 10),
+ Text(
+ "Let's Get Started",
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: MihColors.getSecondaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
+ fontSize: 25,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 15),
+ Text(
+ "Ready to dive in to the world of MIH?\nSign in or create a free MIH account to unlock all the powerful features of the MIH app. It's quick and easy!",
+ style: TextStyle(
+ color: MihColors.getSecondaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
+ fontSize: 15,
+ ),
+ ),
+ const SizedBox(height: 25),
+ Center(
+ child: MihButton(
+ onPressed: () {
+ context.goNamed(
+ 'mihHome',
+ extra: true,
+ );
+ },
+ buttonColor: MihColors.getGreenColor(
+ MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
+ elevation: 10,
+ width: 300,
+ child: Text(
+ "Sign In/ Create Account",
+ style: TextStyle(
+ color: MihColors.getPrimaryColor(
+ MzansiInnovationHub.of(context)!.theme.mode ==
+ "Dark"),
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/mih_ui/lib/mih_package_components/mih_business_profile_preview.dart b/mih_ui/lib/mih_package_components/mih_business_profile_preview.dart
index 81cdcf6b..0380688e 100644
--- a/mih_ui/lib/mih_package_components/mih_business_profile_preview.dart
+++ b/mih_ui/lib/mih_package_components/mih_business_profile_preview.dart
@@ -68,6 +68,7 @@ class _MihBusinessProfilePreviewState extends State {
: MihCircleAvatar(
imageFile: widget.imageFile,
width: profilePictureWidth,
+ expandable: false,
editable: false,
fileNameController: TextEditingController(),
userSelectedfile: null,
diff --git a/mih_ui/lib/mih_package_components/mih_circle_avatar.dart b/mih_ui/lib/mih_package_components/mih_circle_avatar.dart
index c44c72a3..c3d4db1f 100644
--- a/mih_ui/lib/mih_package_components/mih_circle_avatar.dart
+++ b/mih_ui/lib/mih_package_components/mih_circle_avatar.dart
@@ -2,13 +2,17 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:ken_logger/ken_logger.dart';
import 'package:mzansi_innovation_hub/main.dart';
import 'package:mzansi_innovation_hub/mih_package_components/mih_icons.dart';
import 'package:mzansi_innovation_hub/mih_config/mih_colors.dart';
+import 'package:mzansi_innovation_hub/mih_package_components/mih_package_window.dart';
class MihCircleAvatar extends StatefulWidget {
final ImageProvider