Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee7cfdfeaa |
@@ -4,10 +4,10 @@ android {
|
|||||||
namespace = "io.ionic.starter"
|
namespace = "io.ionic.starter"
|
||||||
compileSdk = rootProject.ext.compileSdkVersion
|
compileSdk = rootProject.ext.compileSdkVersion
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "io.ionic.starter"
|
applicationId "com.qoutelink.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 3
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
manifestPlaceholders = [googleMapsApiKey: System.getenv("VITE_GOOGLE_MAPS_KEY") ?: ""]
|
manifestPlaceholders = [googleMapsApiKey: System.getenv("VITE_GOOGLE_MAPS_KEY") ?: ""]
|
||||||
|
|||||||
@@ -21,6 +21,12 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="com.qoutelink.app" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#FFFFFF</color>
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="splashBackground">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">bringg</string>
|
<string name="app_name">Qoutelink</string>
|
||||||
<string name="title_activity_main">bringg</string>
|
<string name="title_activity_main">Qoutelink</string>
|
||||||
<string name="package_name">io.ionic.starter</string>
|
<string name="package_name">io.ionic.starter</string>
|
||||||
<string name="custom_url_scheme">io.ionic.starter</string>
|
<string name="custom_url_scheme">io.ionic.starter</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
<item name="android:background">@drawable/splash</item>
|
<item name="windowSplashScreenBackground">@color/splashBackground</item>
|
||||||
|
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_logo</item>
|
||||||
|
<item name="postSplashScreenTheme">@style/AppTheme.NoActionBar</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -15,5 +15,4 @@ ext {
|
|||||||
cordovaAndroidVersion = '14.0.1'
|
cordovaAndroidVersion = '14.0.1'
|
||||||
rgcfaIncludeGoogle = true
|
rgcfaIncludeGoogle = true
|
||||||
androidxCredentialsVersion = '1.3.0'
|
androidxCredentialsVersion = '1.3.0'
|
||||||
javaVersion = JavaVersion.VERSION_17
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { CapacitorConfig } from '@capacitor/cli';
|
import type { CapacitorConfig } from '@capacitor/cli';
|
||||||
|
|
||||||
const config: CapacitorConfig = {
|
const config: CapacitorConfig = {
|
||||||
appId: 'com.bringg.app',
|
appId: 'com.qoutelink.app',
|
||||||
appName: 'bringg',
|
appName: 'Qoutelink',
|
||||||
webDir: 'dist',
|
webDir: 'dist',
|
||||||
plugins: {
|
plugins: {
|
||||||
FirebaseAuthentication: {
|
FirebaseAuthentication: {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
1A4EB3C21FED79650016851F /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
|
||||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||||
958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; };
|
958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||||
504EC3131FED79650016851F /* Info.plist */,
|
504EC3131FED79650016851F /* Info.plist */,
|
||||||
|
1A4EB3C21FED79650016851F /* App.entitlements */,
|
||||||
2FAD9762203C412B000D30F8 /* config.xml */,
|
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||||
50B271D01FEDC1A000F3C39B /* public */,
|
50B271D01FEDC1A000F3C39B /* public */,
|
||||||
);
|
);
|
||||||
@@ -295,8 +297,9 @@
|
|||||||
baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */;
|
baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -305,7 +308,7 @@
|
|||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.bringg.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.qoutelink.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -317,8 +320,9 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -326,7 +330,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.bringg.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.qoutelink.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|||||||
@@ -1,14 +1,122 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images": [
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-512@2x.png",
|
"idiom": "iphone",
|
||||||
"idiom" : "universal",
|
"scale": "1x",
|
||||||
"platform" : "ios",
|
"size": "20x20",
|
||||||
"size" : "1024x1024"
|
"filename": "icon-20.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "ipad",
|
||||||
|
"scale": "1x",
|
||||||
|
"size": "20x20",
|
||||||
|
"filename": "icon-20.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "iphone",
|
||||||
|
"scale": "1x",
|
||||||
|
"size": "29x29",
|
||||||
|
"filename": "icon-29.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "ipad",
|
||||||
|
"scale": "1x",
|
||||||
|
"size": "29x29",
|
||||||
|
"filename": "icon-29.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "ipad",
|
||||||
|
"scale": "1x",
|
||||||
|
"size": "40x40",
|
||||||
|
"filename": "icon-40.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "iphone",
|
||||||
|
"scale": "2x",
|
||||||
|
"size": "20x20",
|
||||||
|
"filename": "icon-40.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "iphone",
|
||||||
|
"scale": "2x",
|
||||||
|
"size": "29x29",
|
||||||
|
"filename": "icon-58.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "ipad",
|
||||||
|
"scale": "2x",
|
||||||
|
"size": "29x29",
|
||||||
|
"filename": "icon-58.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "iphone",
|
||||||
|
"scale": "3x",
|
||||||
|
"size": "20x20",
|
||||||
|
"filename": "icon-60.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "ipad",
|
||||||
|
"scale": "1x",
|
||||||
|
"size": "76x76",
|
||||||
|
"filename": "icon-76.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "iphone",
|
||||||
|
"scale": "2x",
|
||||||
|
"size": "40x40",
|
||||||
|
"filename": "icon-80.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "ipad",
|
||||||
|
"scale": "2x",
|
||||||
|
"size": "40x40",
|
||||||
|
"filename": "icon-80.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "iphone",
|
||||||
|
"scale": "3x",
|
||||||
|
"size": "29x29",
|
||||||
|
"filename": "icon-87.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "iphone",
|
||||||
|
"scale": "2x",
|
||||||
|
"size": "60x60",
|
||||||
|
"filename": "icon-120.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "iphone",
|
||||||
|
"scale": "3x",
|
||||||
|
"size": "40x40",
|
||||||
|
"filename": "icon-120.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "ipad",
|
||||||
|
"scale": "2x",
|
||||||
|
"size": "76x76",
|
||||||
|
"filename": "icon-152.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "ipad",
|
||||||
|
"scale": "2x",
|
||||||
|
"size": "83.5x83.5",
|
||||||
|
"filename": "icon-167.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "iphone",
|
||||||
|
"scale": "3x",
|
||||||
|
"size": "60x60",
|
||||||
|
"filename": "icon-180.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "ios-marketing",
|
||||||
|
"scale": "1x",
|
||||||
|
"size": "1024x1024",
|
||||||
|
"filename": "icon-1024.png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info": {
|
||||||
"author" : "xcode",
|
"author": "appsuite",
|
||||||
"version" : 1
|
"version": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 890 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 809 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -1,23 +1,13 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images": [
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom": "universal",
|
||||||
"filename" : "splash-2732x2732-2.png",
|
"filename": "splash.png",
|
||||||
"scale" : "1x"
|
"scale": "1x"
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "splash-2732x2732-1.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "splash-2732x2732.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info": {
|
||||||
"version" : 1,
|
"author": "appsuite",
|
||||||
"author" : "xcode"
|
"version": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 480 KiB |
@@ -7,7 +7,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>bringg</string>
|
<string>Qoutelink</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -47,14 +47,18 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>We need access to your photo library so you can attach site photos and references to your requests.</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>We need access to your camera so you can take site photos for your requests.</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>com.bringg.app</string>
|
<string>com.qoutelink.app</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>com.bringg.app</string>
|
<string>com.qoutelink.app</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
|||||||
@@ -18,12 +18,14 @@
|
|||||||
"@capacitor/haptics": "8.0.2",
|
"@capacitor/haptics": "8.0.2",
|
||||||
"@capacitor/ios": "8.3.4",
|
"@capacitor/ios": "8.3.4",
|
||||||
"@capacitor/keyboard": "8.0.3",
|
"@capacitor/keyboard": "8.0.3",
|
||||||
|
"@capacitor/preferences": "^8.0.1",
|
||||||
"@capacitor/push-notifications": "^8.1.1",
|
"@capacitor/push-notifications": "^8.1.1",
|
||||||
"@capacitor/status-bar": "8.0.2",
|
"@capacitor/status-bar": "8.0.2",
|
||||||
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@ionic/pwa-elements": "^3.0.0",
|
"@ionic/pwa-elements": "^3.0.0",
|
||||||
"@ionic/react": "^8.5.0",
|
"@ionic/react": "8.8.7",
|
||||||
"@ionic/react-router": "^8.5.0",
|
"@ionic/react-router": "8.8.7",
|
||||||
"@supabase/supabase-js": "^2.0.0",
|
"@supabase/supabase-js": "^2.108.1",
|
||||||
"firebase": "^11.0.0",
|
"firebase": "^11.0.0",
|
||||||
"ionicons": "^7.4.0",
|
"ionicons": "^7.4.0",
|
||||||
"katex": "^0.16.0",
|
"katex": "^0.16.0",
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
import { IonApp } from '@ionic/react';
|
||||||
import { IonApp, IonRouterOutlet } from '@ionic/react';
|
|
||||||
import { IonReactRouter } from '@ionic/react-router';
|
import { IonReactRouter } from '@ionic/react-router';
|
||||||
|
|
||||||
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
|
import { ProfileProvider, useProfile } from './contexts/ProfileContext';
|
||||||
|
import AppRoutes from './AppRoutes';
|
||||||
|
import { setStatusBarStyle, Style } from './utils/statusBar';
|
||||||
|
|
||||||
|
/* Core CSS required for Ionic components to work properly */
|
||||||
import '@ionic/react/css/core.css';
|
import '@ionic/react/css/core.css';
|
||||||
|
|
||||||
|
/* Basic CSS for apps built with Ionic */
|
||||||
import '@ionic/react/css/normalize.css';
|
import '@ionic/react/css/normalize.css';
|
||||||
import '@ionic/react/css/structure.css';
|
import '@ionic/react/css/structure.css';
|
||||||
import '@ionic/react/css/typography.css';
|
import '@ionic/react/css/typography.css';
|
||||||
|
|
||||||
|
/* Optional CSS utils that can be commented out */
|
||||||
import '@ionic/react/css/padding.css';
|
import '@ionic/react/css/padding.css';
|
||||||
import '@ionic/react/css/float-elements.css';
|
import '@ionic/react/css/float-elements.css';
|
||||||
import '@ionic/react/css/text-alignment.css';
|
import '@ionic/react/css/text-alignment.css';
|
||||||
@@ -14,60 +23,34 @@ import '@ionic/react/css/text-transformation.css';
|
|||||||
import '@ionic/react/css/flex-utils.css';
|
import '@ionic/react/css/flex-utils.css';
|
||||||
import '@ionic/react/css/display.css';
|
import '@ionic/react/css/display.css';
|
||||||
|
|
||||||
|
/* Theme variables */
|
||||||
import './theme/variables.css';
|
import './theme/variables.css';
|
||||||
|
|
||||||
import Home from './pages/Home';
|
const AppShell: React.FC = () => {
|
||||||
import ProductDetail from './pages/ProductDetail';
|
const { themeMode } = useProfile();
|
||||||
import Cart from './pages/Cart';
|
|
||||||
import OrderConfirmation from './pages/OrderConfirmation';
|
|
||||||
import MyOrders from './pages/MyOrders';
|
|
||||||
import OrderDetail from './pages/OrderDetail';
|
|
||||||
import Auth from './pages/Auth';
|
|
||||||
import Profile from './pages/Profile';
|
|
||||||
import Addresses from './pages/Addresses';
|
|
||||||
import Wallet from './pages/Wallet';
|
|
||||||
import PaymentMethods from './pages/PaymentMethods';
|
|
||||||
import AddPaymentMethod from './pages/AddPaymentMethod';
|
|
||||||
import AdminOrders from './pages/AdminOrders';
|
|
||||||
import AdminOrderDetail from './pages/AdminOrderDetail';
|
|
||||||
import { AuthProvider } from './context/AuthContext';
|
|
||||||
|
|
||||||
const App: React.FC = () => (
|
useEffect(() => {
|
||||||
<IonApp>
|
document.body.classList.toggle('dark', themeMode === 'dark');
|
||||||
|
setStatusBarStyle(themeMode === 'dark' ? Style.Dark : Style.Light);
|
||||||
|
}, [themeMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
<IonReactRouter>
|
<IonReactRouter>
|
||||||
<AuthProvider>
|
<AppRoutes />
|
||||||
<IonRouterOutlet>
|
|
||||||
<Route path="/home" component={Home} exact />
|
|
||||||
<Route path="/auth" component={Auth} exact />
|
|
||||||
<Route path="/product/:id" component={ProductDetail} exact />
|
|
||||||
<Route path="/cart" component={Cart} exact />
|
|
||||||
<Route path="/orders" component={MyOrders} exact />
|
|
||||||
<Route path="/orders/:id" component={OrderDetail} exact />
|
|
||||||
<Route path="/profile" component={Profile} exact />
|
|
||||||
<Route path="/addresses" component={Addresses} exact />
|
|
||||||
<Route path="/wallet" component={Wallet} exact />
|
|
||||||
<Route
|
|
||||||
path="/wallet/payment-methods"
|
|
||||||
component={PaymentMethods}
|
|
||||||
exact
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/wallet/payment-methods/add"
|
|
||||||
component={AddPaymentMethod}
|
|
||||||
exact
|
|
||||||
/>
|
|
||||||
<Route path="/admin/orders" component={AdminOrders} exact />
|
|
||||||
<Route path="/admin/orders/:id" component={AdminOrderDetail} exact />
|
|
||||||
<Route
|
|
||||||
path="/order-confirmation"
|
|
||||||
component={OrderConfirmation}
|
|
||||||
exact
|
|
||||||
/>
|
|
||||||
<Redirect exact from="/" to="/home" />
|
|
||||||
</IonRouterOutlet>
|
|
||||||
</AuthProvider>
|
|
||||||
</IonReactRouter>
|
</IonReactRouter>
|
||||||
</IonApp>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<IonApp>
|
||||||
|
<AuthProvider>
|
||||||
|
<ProfileProvider>
|
||||||
|
<AppShell />
|
||||||
|
</ProfileProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</IonApp>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Route, Redirect } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
IonRouterOutlet,
|
||||||
|
IonTabs,
|
||||||
|
IonTabBar,
|
||||||
|
IonTabButton,
|
||||||
|
IonIcon,
|
||||||
|
IonLabel,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import {
|
||||||
|
homeOutline,
|
||||||
|
documentTextOutline,
|
||||||
|
personOutline,
|
||||||
|
} from 'ionicons/icons';
|
||||||
|
import { useAuth } from './contexts/AuthContext';
|
||||||
|
|
||||||
|
import Auth from './pages/Auth';
|
||||||
|
import VerifyEmail from './pages/VerifyEmail';
|
||||||
|
import ForgotPassword from './pages/ForgotPassword';
|
||||||
|
import VerifyReset from './pages/VerifyReset';
|
||||||
|
import SetupProfile from './pages/SetupProfile';
|
||||||
|
import Home from './pages/Home';
|
||||||
|
import BuyerRequests from './pages/BuyerRequests';
|
||||||
|
import NewRfq from './pages/NewRfq';
|
||||||
|
import RfqDetail from './pages/RfqDetail';
|
||||||
|
import SupplierQuoteForm from './pages/SupplierQuoteForm';
|
||||||
|
import BuyerQuoteList from './pages/BuyerQuoteList';
|
||||||
|
import Profile from './pages/Profile';
|
||||||
|
import EditProfile from './pages/EditProfile';
|
||||||
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
|
|
||||||
|
const AppRoutes: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IonRouterOutlet>
|
||||||
|
{/* Auth routes */}
|
||||||
|
<Route path="/auth" component={Auth} exact />
|
||||||
|
<Route path="/verify-email" component={VerifyEmail} exact />
|
||||||
|
<Route path="/forgot-password" component={ForgotPassword} exact />
|
||||||
|
<Route path="/verify-reset" component={VerifyReset} exact />
|
||||||
|
|
||||||
|
{/* Setup Profile (guarded manually by user presence, not ProtectedRoute to avoid loop) */}
|
||||||
|
<Route
|
||||||
|
path="/setup-profile"
|
||||||
|
exact
|
||||||
|
render={() => (!user ? <Redirect to="/auth" /> : <SetupProfile />)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Pushed routes without tab bar */}
|
||||||
|
<ProtectedRoute path="/rfq/new" component={NewRfq} exact />
|
||||||
|
<ProtectedRoute
|
||||||
|
path="/rfq/:id([0-9a-fA-F-]{36})"
|
||||||
|
component={RfqDetail}
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
<ProtectedRoute
|
||||||
|
path="/quote/:id([0-9a-fA-F-]{36})"
|
||||||
|
component={SupplierQuoteForm}
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
<ProtectedRoute
|
||||||
|
path="/quote-list/:id([0-9a-fA-F-]{36})"
|
||||||
|
component={BuyerQuoteList}
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
<ProtectedRoute path="/profile/edit" component={EditProfile} exact />
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<ProtectedRoute
|
||||||
|
path={['/home', '/requests', '/profile']}
|
||||||
|
component={() => (
|
||||||
|
<IonTabs>
|
||||||
|
<IonRouterOutlet>
|
||||||
|
<Route path="/home" component={Home} exact />
|
||||||
|
<Route path="/requests" component={BuyerRequests} exact />
|
||||||
|
<Route path="/profile" component={Profile} exact />
|
||||||
|
</IonRouterOutlet>
|
||||||
|
|
||||||
|
<IonTabBar slot="bottom">
|
||||||
|
<IonTabButton tab="home" href="/home">
|
||||||
|
<IonIcon icon={homeOutline} />
|
||||||
|
<IonLabel>Home</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
<IonTabButton tab="requests" href="/requests">
|
||||||
|
<IonIcon icon={documentTextOutline} />
|
||||||
|
<IonLabel>Requests</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
<IonTabButton tab="profile" href="/profile">
|
||||||
|
<IonIcon icon={personOutline} />
|
||||||
|
<IonLabel>Profile</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
</IonTabBar>
|
||||||
|
</IonTabs>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Default Redirect */}
|
||||||
|
<Route exact path="/" render={() => <Redirect to="/home" />} />
|
||||||
|
</IonRouterOutlet>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppRoutes;
|
||||||
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 839 KiB |
|
Before Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 501 KiB |
|
Before Width: | Height: | Size: 602 KiB |
|
Before Width: | Height: | Size: 554 KiB |
|
Before Width: | Height: | Size: 553 KiB |
|
Before Width: | Height: | Size: 437 KiB |
|
Before Width: | Height: | Size: 676 KiB |
|
Before Width: | Height: | Size: 673 KiB |
|
Before Width: | Height: | Size: 452 KiB |
|
Before Width: | Height: | Size: 545 KiB |
|
Before Width: | Height: | Size: 550 KiB |
|
Before Width: | Height: | Size: 461 KiB |
|
Before Width: | Height: | Size: 689 KiB |
|
Before Width: | Height: | Size: 447 KiB |
|
Before Width: | Height: | Size: 690 KiB |
|
Before Width: | Height: | Size: 599 KiB |
|
Before Width: | Height: | Size: 464 KiB |
|
Before Width: | Height: | Size: 629 KiB |
|
Before Width: | Height: | Size: 515 KiB |
|
Before Width: | Height: | Size: 655 KiB |
|
Before Width: | Height: | Size: 697 KiB |
|
Before Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 466 KiB |
@@ -1,36 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Redirect, Route, RouteProps } from 'react-router-dom';
|
|
||||||
import { IonLoading } from '@ionic/react';
|
|
||||||
import { useAuth } from '../context/AuthContext';
|
|
||||||
|
|
||||||
type Props = RouteProps;
|
|
||||||
|
|
||||||
const AdminRoute: React.FC<Props> = ({ component: Component, ...rest }) => {
|
|
||||||
const { user, profile, isLoading } = useAuth();
|
|
||||||
|
|
||||||
if (!Component) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
{...rest}
|
|
||||||
render={(props) => {
|
|
||||||
if (isLoading) {
|
|
||||||
return <IonLoading isOpen message="Loading..." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return <Redirect to="/auth" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!profile?.is_admin) {
|
|
||||||
return <Redirect to="/home" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RouteComponent = Component as React.ComponentType<any>;
|
|
||||||
return <RouteComponent {...props} />;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminRoute;
|
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { IonIcon } from '@ionic/react';
|
||||||
|
|
||||||
|
interface DashboardMetricCardProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
hint: string;
|
||||||
|
icon: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardMetricCard: React.FC<DashboardMetricCardProps> = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
hint,
|
||||||
|
icon,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-surface)',
|
||||||
|
borderRadius: '24px',
|
||||||
|
padding: '20px',
|
||||||
|
flex: '1',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '12px',
|
||||||
|
cursor: onClick ? 'pointer' : 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '700',
|
||||||
|
color: 'var(--color-text-tertiary)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.05em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '36px',
|
||||||
|
height: '36px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
background: 'var(--color-brand-medium)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IonIcon
|
||||||
|
icon={icon}
|
||||||
|
style={{ color: 'var(--color-brand)', fontSize: '18px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '28px',
|
||||||
|
fontWeight: '400',
|
||||||
|
color: 'var(--color-text-primary)',
|
||||||
|
lineHeight: '1',
|
||||||
|
fontVariantNumeric: 'tabular-nums',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '400',
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
marginTop: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hint}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardMetricCard;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { IonInput } from '@ionic/react';
|
||||||
|
|
||||||
|
interface OtpInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OtpInput: React.FC<OtpInputProps> = ({ value, onChange, disabled }) => {
|
||||||
|
const inputsRef = useRef<(HTMLInputElement | null)[]>([]);
|
||||||
|
|
||||||
|
// Convert current value string into an array of 6 chars
|
||||||
|
const digits = value.split('').concat(Array(6).fill('')).slice(0, 6);
|
||||||
|
|
||||||
|
const handleChange = (e: any, index: number) => {
|
||||||
|
let val = e.target.value;
|
||||||
|
// Allow only numeric
|
||||||
|
if (!/^[0-9]*$/.test(val)) return;
|
||||||
|
|
||||||
|
// Grab the last character if they typed multiple
|
||||||
|
val = val.substring(val.length - 1);
|
||||||
|
|
||||||
|
const newDigits = [...digits];
|
||||||
|
newDigits[index] = val;
|
||||||
|
onChange(newDigits.join(''));
|
||||||
|
|
||||||
|
// Move to next input if filled
|
||||||
|
if (val !== '' && index < 5) {
|
||||||
|
const nextInput = inputsRef.current[index + 1];
|
||||||
|
if (nextInput) nextInput.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (
|
||||||
|
e: React.KeyboardEvent<HTMLInputElement>,
|
||||||
|
index: number
|
||||||
|
) => {
|
||||||
|
if (e.key === 'Backspace' && !digits[index] && index > 0) {
|
||||||
|
// Move to previous input on backspace if current is empty
|
||||||
|
const prevInput = inputsRef.current[index - 1];
|
||||||
|
if (prevInput) {
|
||||||
|
prevInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>
|
||||||
|
{digits.map((digit, index) => (
|
||||||
|
<input
|
||||||
|
key={index}
|
||||||
|
ref={(el) => {
|
||||||
|
inputsRef.current[index] = el;
|
||||||
|
}}
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
maxLength={2}
|
||||||
|
value={digit}
|
||||||
|
onChange={(e) => handleChange(e, index)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, index)}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '48px',
|
||||||
|
height: '52px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: 'none',
|
||||||
|
background: '#f4f7f4',
|
||||||
|
color: '#0f1720',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '24px',
|
||||||
|
fontWeight: '600',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OtpInput;
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
IonButton,
|
|
||||||
IonIcon,
|
|
||||||
IonLabel,
|
|
||||||
IonModal,
|
|
||||||
IonRadio,
|
|
||||||
IonRadioGroup,
|
|
||||||
IonItem,
|
|
||||||
IonList,
|
|
||||||
} from '@ionic/react';
|
|
||||||
import { funnelOutline } from 'ionicons/icons';
|
|
||||||
|
|
||||||
export type SortOption = 'popular' | 'price-asc' | 'price-desc' | 'name-asc';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
categories: string[];
|
|
||||||
selectedCategory: string;
|
|
||||||
setSelectedCategory: (value: string) => void;
|
|
||||||
sortOption: SortOption;
|
|
||||||
setSortOption: (value: SortOption) => void;
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProductFilters: React.FC<Props> = ({
|
|
||||||
categories,
|
|
||||||
selectedCategory,
|
|
||||||
setSelectedCategory,
|
|
||||||
sortOption,
|
|
||||||
setSortOption,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
}) => (
|
|
||||||
<>
|
|
||||||
<div className="filter-row">
|
|
||||||
<div className="filter-chip-row">
|
|
||||||
<IonButton
|
|
||||||
fill={selectedCategory === 'All' ? 'solid' : 'outline'}
|
|
||||||
size="small"
|
|
||||||
onClick={() => setSelectedCategory('All')}
|
|
||||||
>
|
|
||||||
All
|
|
||||||
</IonButton>
|
|
||||||
{categories.map((category) => (
|
|
||||||
<IonButton
|
|
||||||
key={category}
|
|
||||||
fill={selectedCategory === category ? 'solid' : 'outline'}
|
|
||||||
size="small"
|
|
||||||
onClick={() => setSelectedCategory(category)}
|
|
||||||
>
|
|
||||||
{category}
|
|
||||||
</IonButton>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<IonButton fill="clear" className="filter-button" onClick={onClose}>
|
|
||||||
<IonIcon icon={funnelOutline} slot="start" />
|
|
||||||
Filters
|
|
||||||
</IonButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<IonModal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onDidDismiss={onClose}
|
|
||||||
breakpoints={[0, 0.45]}
|
|
||||||
initialBreakpoint={0.45}
|
|
||||||
>
|
|
||||||
<div className="filter-modal ion-padding">
|
|
||||||
<h2>Sort products</h2>
|
|
||||||
<IonRadioGroup
|
|
||||||
value={sortOption}
|
|
||||||
onIonChange={(e) => setSortOption(e.detail.value)}
|
|
||||||
>
|
|
||||||
<IonList lines="none">
|
|
||||||
<IonItem>
|
|
||||||
<IonLabel>Most popular</IonLabel>
|
|
||||||
<IonRadio value="popular" />
|
|
||||||
</IonItem>
|
|
||||||
<IonItem>
|
|
||||||
<IonLabel>Price: low to high</IonLabel>
|
|
||||||
<IonRadio value="price-asc" />
|
|
||||||
</IonItem>
|
|
||||||
<IonItem>
|
|
||||||
<IonLabel>Price: high to low</IonLabel>
|
|
||||||
<IonRadio value="price-desc" />
|
|
||||||
</IonItem>
|
|
||||||
<IonItem>
|
|
||||||
<IonLabel>Name: A to Z</IonLabel>
|
|
||||||
<IonRadio value="name-asc" />
|
|
||||||
</IonItem>
|
|
||||||
</IonList>
|
|
||||||
</IonRadioGroup>
|
|
||||||
<IonButton expand="block" onClick={onClose}>
|
|
||||||
Done
|
|
||||||
</IonButton>
|
|
||||||
</div>
|
|
||||||
</IonModal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ProductFilters;
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { IonIcon } from '@ionic/react';
|
||||||
|
|
||||||
|
interface ProfileInfoRowProps {
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
value?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileInfoRow: React.FC<ProfileInfoRowProps> = ({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
minHeight: '64px',
|
||||||
|
padding: '10px 20px',
|
||||||
|
cursor: onClick ? 'pointer' : 'default',
|
||||||
|
gap: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '14px',
|
||||||
|
minWidth: 0,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
background: 'var(--color-surface-raised)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IonIcon
|
||||||
|
icon={icon}
|
||||||
|
style={{ color: 'var(--color-brand)', fontSize: '20px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '2px',
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '700',
|
||||||
|
color: 'var(--color-text-tertiary)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.04em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
{value && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '15px',
|
||||||
|
fontWeight: '400',
|
||||||
|
color: 'var(--color-text-primary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileInfoRow;
|
||||||
@@ -1,29 +1,54 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Redirect, Route, RouteProps } from 'react-router-dom';
|
import { Route, Redirect, RouteProps } from 'react-router-dom';
|
||||||
import { IonLoading } from '@ionic/react';
|
import { IonSpinner } from '@ionic/react';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { useProfile } from '../contexts/ProfileContext';
|
||||||
|
import { useProfileLoader } from '../hooks/useProfileLoader';
|
||||||
|
|
||||||
type Props = RouteProps;
|
interface ProtectedRouteProps extends RouteProps {
|
||||||
|
component: React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
const ProtectedRoute: React.FC<Props> = ({ component: Component, ...rest }) => {
|
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||||
const { user, isLoading } = useAuth();
|
component: Component,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { status } = useProfile();
|
||||||
|
const { loadProfileAndNavigate } = useProfileLoader();
|
||||||
|
|
||||||
if (!Component) return null;
|
useEffect(() => {
|
||||||
|
// If user is logged in but profile is still loading (e.g. cold start direct link)
|
||||||
|
if (user && status === 'loading') {
|
||||||
|
loadProfileAndNavigate();
|
||||||
|
}
|
||||||
|
}, [user, status, loadProfileAndNavigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
{...rest}
|
{...rest}
|
||||||
render={(props) => {
|
render={(props) => {
|
||||||
if (isLoading) {
|
|
||||||
return <IonLoading isOpen message="Loading..." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <Redirect to="/auth" />;
|
return <Redirect to="/auth" />;
|
||||||
}
|
}
|
||||||
|
if (status === 'loading') {
|
||||||
const RouteComponent = Component as React.ComponentType<any>;
|
return (
|
||||||
return <RouteComponent {...props} />;
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IonSpinner name="crescent" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === 'missing') {
|
||||||
|
return <Redirect to="/setup-profile" />;
|
||||||
|
}
|
||||||
|
return <Component {...props} />;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface QuotationCardProps {
|
||||||
|
supplierName: string;
|
||||||
|
priceLabel: string;
|
||||||
|
deliveryOption: string;
|
||||||
|
status: string;
|
||||||
|
onAccept: () => void;
|
||||||
|
onDecline: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QuotationCard: React.FC<QuotationCardProps> = ({
|
||||||
|
supplierName,
|
||||||
|
priceLabel,
|
||||||
|
deliveryOption,
|
||||||
|
status,
|
||||||
|
onAccept,
|
||||||
|
onDecline,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-surface)',
|
||||||
|
borderRadius: '24px',
|
||||||
|
padding: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: 'var(--color-text-primary)',
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{supplierName}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '30px',
|
||||||
|
fontWeight: '400',
|
||||||
|
color: 'var(--color-brand-strong)',
|
||||||
|
fontVariantNumeric: 'tabular-nums',
|
||||||
|
marginBottom: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{priceLabel}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '400',
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
marginBottom: '18px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{deliveryOption}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status === 'submitted' ? (
|
||||||
|
<div style={{ display: 'flex', gap: '12px' }}>
|
||||||
|
<button
|
||||||
|
onClick={onAccept}
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-brand)',
|
||||||
|
color: '#ffffff',
|
||||||
|
borderRadius: '999px',
|
||||||
|
padding: '12px 16px',
|
||||||
|
border: 'none',
|
||||||
|
flex: 1,
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: '14px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onDecline}
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-surface-raised)',
|
||||||
|
color: 'var(--color-brand)',
|
||||||
|
borderRadius: '999px',
|
||||||
|
padding: '12px 16px',
|
||||||
|
border: 'none',
|
||||||
|
flex: 1,
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: '14px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Decline
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
background:
|
||||||
|
status === 'accepted'
|
||||||
|
? 'var(--color-success-soft)'
|
||||||
|
: 'rgba(23,33,43,0.10)',
|
||||||
|
color:
|
||||||
|
status === 'accepted' ? 'var(--color-success-text)' : '#4b5563',
|
||||||
|
borderRadius: '999px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '700',
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuotationCard;
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface QuoteSummaryRowProps {
|
||||||
|
supplierName: string;
|
||||||
|
priceLabel: string;
|
||||||
|
deliveryOption: string;
|
||||||
|
status: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QuoteSummaryRow: React.FC<QuoteSummaryRowProps> = ({
|
||||||
|
supplierName,
|
||||||
|
priceLabel,
|
||||||
|
deliveryOption,
|
||||||
|
status,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '4px',
|
||||||
|
padding: '12px 0',
|
||||||
|
borderBottom: '1px solid var(--color-border)',
|
||||||
|
cursor: onClick ? 'pointer' : 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '15px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: 'var(--color-text-primary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{supplierName}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '15px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: 'var(--color-brand-strong)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{priceLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '400',
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{deliveryOption}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color:
|
||||||
|
status === 'accepted'
|
||||||
|
? 'var(--color-success-text)'
|
||||||
|
: 'var(--color-text-secondary)',
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuoteSummaryRow;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface RecentActivityCardProps {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
meta: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecentActivityCard: React.FC<RecentActivityCardProps> = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
meta,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-shell)',
|
||||||
|
borderRadius: '38px',
|
||||||
|
padding: '5px',
|
||||||
|
margin: '0 20px 12px',
|
||||||
|
boxShadow: 'var(--color-shadow-lg)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-surface)',
|
||||||
|
borderRadius: '32px',
|
||||||
|
padding: '18px 20px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
boxShadow: 'var(--color-shadow-sm)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '4px',
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: '400',
|
||||||
|
color: 'var(--color-text-primary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '400',
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: '400',
|
||||||
|
color: 'var(--color-brand-strong)',
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{meta}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecentActivityCard;
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface RequestListCardProps {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
referenceNumber: string;
|
||||||
|
status: string;
|
||||||
|
trailingText?: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RequestListCard: React.FC<RequestListCardProps> = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
referenceNumber,
|
||||||
|
status,
|
||||||
|
trailingText,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
let statusBg = 'rgba(23,33,43,0.10)';
|
||||||
|
let statusColor = '#4b5563';
|
||||||
|
|
||||||
|
if (status === 'open') {
|
||||||
|
statusBg = 'var(--color-success-soft)';
|
||||||
|
statusColor = 'var(--color-success-text)';
|
||||||
|
} else if (status === 'quoted') {
|
||||||
|
statusBg = 'var(--color-warning-soft)';
|
||||||
|
statusColor = 'var(--color-warning-text)';
|
||||||
|
} else if (status === 'closed') {
|
||||||
|
statusBg = 'rgba(23,33,43,0.10)';
|
||||||
|
statusColor = '#4b5563';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-surface)',
|
||||||
|
borderRadius: '24px',
|
||||||
|
padding: '20px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '4px',
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: 'var(--color-text-primary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '400',
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{trailingText && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: 'var(--color-brand-strong)',
|
||||||
|
marginLeft: '12px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{trailingText}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-accent-soft)',
|
||||||
|
color: 'var(--color-warning-text)',
|
||||||
|
borderRadius: '999px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '700',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{referenceNumber}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: statusBg,
|
||||||
|
color: statusColor,
|
||||||
|
borderRadius: '999px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '700',
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RequestListCard;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { IonIcon } from '@ionic/react';
|
||||||
|
import { checkmarkCircle, ellipseOutline } from 'ionicons/icons';
|
||||||
|
|
||||||
|
export interface SupplierOption {
|
||||||
|
id: string;
|
||||||
|
full_name: string;
|
||||||
|
company_name: string | null;
|
||||||
|
city: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SupplierPickerRowProps {
|
||||||
|
supplier: SupplierOption;
|
||||||
|
selected: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SupplierPickerRow: React.FC<SupplierPickerRowProps> = ({
|
||||||
|
supplier,
|
||||||
|
selected,
|
||||||
|
onToggle,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onToggle}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '12px 0',
|
||||||
|
borderBottom: '1px solid #edf4ee',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IonIcon
|
||||||
|
icon={selected ? checkmarkCircle : ellipseOutline}
|
||||||
|
style={{
|
||||||
|
fontSize: '24px',
|
||||||
|
color: selected ? '#166534' : '#8a978f',
|
||||||
|
marginRight: '12px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '2px',
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: '15px', fontWeight: '600', color: '#0f1720' }}>
|
||||||
|
{supplier.company_name || supplier.full_name}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: '13px', fontWeight: '400', color: '#6c7a71' }}>
|
||||||
|
{supplier.company_name ? supplier.full_name : 'Independent'}
|
||||||
|
{supplier.city ? ` • ${supplier.city}` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SupplierPickerRow;
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import type { Session, User } from '@supabase/supabase-js';
|
|
||||||
import { supabase } from '../supabase';
|
|
||||||
import type { Tables } from '../database.types';
|
|
||||||
|
|
||||||
type Profile = Tables<'profiles'>;
|
|
||||||
|
|
||||||
type AuthContextValue = {
|
|
||||||
session: Session | null;
|
|
||||||
user: User | null;
|
|
||||||
profile: Profile | null;
|
|
||||||
isLoading: boolean;
|
|
||||||
refreshProfile: () => Promise<void>;
|
|
||||||
signOut: () => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
|
||||||
|
|
||||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
|
||||||
const [user, setUser] = useState<User | null>(null);
|
|
||||||
const [profile, setProfile] = useState<Profile | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
const loadProfile = async (userId: string | null) => {
|
|
||||||
if (!userId) {
|
|
||||||
setProfile(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await supabase
|
|
||||||
.from('profiles')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', userId)
|
|
||||||
.maybeSingle();
|
|
||||||
setProfile(data ?? null);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isMounted = true;
|
|
||||||
|
|
||||||
supabase.auth.getSession().then(async ({ data }) => {
|
|
||||||
if (!isMounted) return;
|
|
||||||
const nextSession = data.session ?? null;
|
|
||||||
setSession(nextSession);
|
|
||||||
setUser(nextSession?.user ?? null);
|
|
||||||
await loadProfile(nextSession?.user?.id ?? null);
|
|
||||||
if (isMounted) setIsLoading(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { subscription },
|
|
||||||
} = supabase.auth.onAuthStateChange(async (_event, nextSession) => {
|
|
||||||
setSession(nextSession);
|
|
||||||
setUser(nextSession?.user ?? null);
|
|
||||||
await loadProfile(nextSession?.user?.id ?? null);
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMounted = false;
|
|
||||||
subscription.unsubscribe();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const value = useMemo<AuthContextValue>(
|
|
||||||
() => ({
|
|
||||||
session,
|
|
||||||
user,
|
|
||||||
profile,
|
|
||||||
isLoading,
|
|
||||||
refreshProfile: async () => loadProfile(user?.id ?? null),
|
|
||||||
signOut: async () => {
|
|
||||||
await supabase.auth.signOut();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[session, user, profile, isLoading]
|
|
||||||
);
|
|
||||||
|
|
||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useAuth = () => {
|
|
||||||
const context = useContext(AuthContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useAuth must be used within AuthProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
import { User } from '@supabase/supabase-js';
|
||||||
|
import { supabase } from '../supabase';
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
user: User | null;
|
||||||
|
signOut: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType>({
|
||||||
|
user: null,
|
||||||
|
signOut: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useAuth = () => useContext(AuthContext);
|
||||||
|
|
||||||
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [initializing, setInitializing] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
supabase.auth.getSession().then(({ data }) => {
|
||||||
|
setUser(data.session?.user ?? null);
|
||||||
|
setInitializing(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { subscription },
|
||||||
|
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||||
|
setUser(session?.user ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const signOut = async () => {
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (initializing) {
|
||||||
|
return null; // Or a simple spinner if preferred, but usually handled nicely
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ user, signOut }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||