Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0119c17e34 |
@@ -4,7 +4,7 @@ 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.inspot.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@@ -21,7 +26,19 @@
|
|||||||
<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.inspot.app" />
|
||||||
|
</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.inspot.app" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 742 B |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 708 B |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 742 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -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">InSpot</string>
|
||||||
<string name="title_activity_main">bringg</string>
|
<string name="title_activity_main">InSpot</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>
|
||||||
|
|||||||
@@ -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.inspot.app',
|
||||||
appName: 'bringg',
|
appName: 'InSpot',
|
||||||
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,6 +297,7 @@
|
|||||||
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 = 1;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
@@ -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.inspot.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,6 +320,7 @@
|
|||||||
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 = 1;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
@@ -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.inspot.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,5 +1,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Capacitor
|
import Capacitor
|
||||||
|
import FirebaseCore
|
||||||
|
import FirebaseMessaging
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
@@ -7,10 +9,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
// Override point for customization after application launch.
|
FirebaseApp.configure()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||||
|
Messaging.messaging().apnsToken = deviceToken
|
||||||
|
Messaging.messaging().token { token, error in
|
||||||
|
if let error = error {
|
||||||
|
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
|
||||||
|
} else if let token = token {
|
||||||
|
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||||
|
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
|
||||||
|
}
|
||||||
|
|
||||||
func applicationWillResignActive(_ application: UIApplication) {
|
func applicationWillResignActive(_ application: UIApplication) {
|
||||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||||
|
|||||||
@@ -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: 58 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 480 B |
|
After Width: | Height: | Size: 663 B |
|
After Width: | Height: | Size: 979 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.4 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>InSpot</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -51,12 +51,31 @@
|
|||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>com.bringg.app</string>
|
<string>com.inspot.app</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>com.bringg.app</string>
|
<string>com.inspot.app</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.inspot.app</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>com.inspot.app</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>InSpot needs camera access to let you capture and share reels of local spots.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>InSpot needs photo library access to let you upload profile photos and share reels from your gallery.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>InSpot needs microphone access to record sound for your reels.</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>InSpot needs your location to discover nearby spots and social connections.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -20,13 +20,15 @@
|
|||||||
"@capacitor/keyboard": "8.0.3",
|
"@capacitor/keyboard": "8.0.3",
|
||||||
"@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",
|
||||||
|
"lottie-react": "^2.4.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.54.0",
|
"react-hook-form": "^7.54.0",
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
import { IonApp, IonRouterOutlet } from '@ionic/react';
|
import {
|
||||||
|
IonApp,
|
||||||
|
IonIcon,
|
||||||
|
IonLabel,
|
||||||
|
IonRouterOutlet,
|
||||||
|
IonTabBar,
|
||||||
|
IonTabButton,
|
||||||
|
IonTabs,
|
||||||
|
} from '@ionic/react';
|
||||||
import { IonReactRouter } from '@ionic/react-router';
|
import { IonReactRouter } from '@ionic/react-router';
|
||||||
|
import {
|
||||||
|
flameOutline,
|
||||||
|
locationOutline,
|
||||||
|
chatbubblesOutline,
|
||||||
|
personOutline,
|
||||||
|
} from 'ionicons/icons';
|
||||||
|
|
||||||
|
/* Core CSS required for Ionic components to work properly */
|
||||||
import '@ionic/react/css/core.css';
|
import '@ionic/react/css/core.css';
|
||||||
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,59 +31,122 @@ 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';
|
/* Pages */
|
||||||
import ProductDetail from './pages/ProductDetail';
|
import AuthPage from './pages/AuthPage';
|
||||||
import Cart from './pages/Cart';
|
import VerifyEmailPage from './pages/VerifyEmailPage';
|
||||||
import OrderConfirmation from './pages/OrderConfirmation';
|
import ForgotPasswordPage from './pages/ForgotPasswordPage';
|
||||||
import MyOrders from './pages/MyOrders';
|
import NewPasswordPage from './pages/NewPasswordPage';
|
||||||
import OrderDetail from './pages/OrderDetail';
|
import OnboardingProfilePage from './pages/OnboardingProfilePage';
|
||||||
import Auth from './pages/Auth';
|
import IcebreakerPage from './pages/IcebreakerPage';
|
||||||
import Profile from './pages/Profile';
|
import HangoutPage from './pages/HangoutPage';
|
||||||
import Addresses from './pages/Addresses';
|
import ReelsPage from './pages/ReelsPage';
|
||||||
import Wallet from './pages/Wallet';
|
import CameraPage from './pages/CameraPage';
|
||||||
import PaymentMethods from './pages/PaymentMethods';
|
import ChatsPage from './pages/ChatsPage';
|
||||||
import AddPaymentMethod from './pages/AddPaymentMethod';
|
import ChatThreadPage from './pages/ChatThreadPage';
|
||||||
import AdminOrders from './pages/AdminOrders';
|
import ProfilePage from './pages/ProfilePage';
|
||||||
import AdminOrderDetail from './pages/AdminOrderDetail';
|
import EditProfilePage from './pages/EditProfilePage';
|
||||||
import { AuthProvider } from './context/AuthContext';
|
import NotificationsPage from './pages/NotificationsPage';
|
||||||
|
import PremiumPage from './pages/PremiumPage';
|
||||||
|
|
||||||
|
/* Contexts & Components */
|
||||||
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
|
import { isPreviewMode } from './utils/previewMode';
|
||||||
|
|
||||||
const App: React.FC = () => (
|
const App: React.FC = () => (
|
||||||
<IonApp>
|
<IonApp>
|
||||||
<IonReactRouter>
|
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<IonReactRouter>
|
||||||
<IonRouterOutlet>
|
<IonRouterOutlet>
|
||||||
<Route path="/home" component={Home} exact />
|
{/* Auth Stack */}
|
||||||
<Route path="/auth" component={Auth} exact />
|
<Route path="/auth" component={AuthPage} exact />
|
||||||
<Route path="/product/:id" component={ProductDetail} exact />
|
<Route path="/verify-email" component={VerifyEmailPage} exact />
|
||||||
<Route path="/cart" component={Cart} exact />
|
<Route path="/forgot-password" component={ForgotPasswordPage} exact />
|
||||||
<Route path="/orders" component={MyOrders} exact />
|
<Route path="/new-password" component={NewPasswordPage} exact />
|
||||||
<Route path="/orders/:id" component={OrderDetail} exact />
|
<ProtectedRoute
|
||||||
<Route path="/profile" component={Profile} exact />
|
path="/onboarding/profile"
|
||||||
<Route path="/addresses" component={Addresses} exact />
|
component={OnboardingProfilePage}
|
||||||
<Route path="/wallet" component={Wallet} exact />
|
|
||||||
<Route
|
|
||||||
path="/wallet/payment-methods"
|
|
||||||
component={PaymentMethods}
|
|
||||||
exact
|
exact
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path="/wallet/payment-methods/add"
|
{/* Detailed Stack (No Tabs) */}
|
||||||
component={AddPaymentMethod}
|
<ProtectedRoute path="/reels/:placeId" component={ReelsPage} exact />
|
||||||
|
<ProtectedRoute
|
||||||
|
path="/chat/:conversationId"
|
||||||
|
component={ChatThreadPage}
|
||||||
exact
|
exact
|
||||||
/>
|
/>
|
||||||
<Route path="/admin/orders" component={AdminOrders} exact />
|
<ProtectedRoute
|
||||||
<Route path="/admin/orders/:id" component={AdminOrderDetail} exact />
|
path="/profile/edit"
|
||||||
<Route
|
component={EditProfilePage}
|
||||||
path="/order-confirmation"
|
exact
|
||||||
component={OrderConfirmation}
|
/>
|
||||||
|
<ProtectedRoute path="/premium" component={PremiumPage} exact />
|
||||||
|
<ProtectedRoute
|
||||||
|
path="/notifications"
|
||||||
|
component={NotificationsPage}
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
<ProtectedRoute path="/camera" component={CameraPage} exact />
|
||||||
|
|
||||||
|
{/* Main Tabs */}
|
||||||
|
<Route
|
||||||
|
path={['/icebreaker', '/hangout', '/chats', '/profile']}
|
||||||
|
render={() => (
|
||||||
|
<IonTabs>
|
||||||
|
<IonRouterOutlet>
|
||||||
|
<ProtectedRoute
|
||||||
|
path="/icebreaker"
|
||||||
|
component={IcebreakerPage}
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
<ProtectedRoute
|
||||||
|
path="/hangout"
|
||||||
|
component={HangoutPage}
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
<ProtectedRoute path="/chats" component={ChatsPage} exact />
|
||||||
|
<ProtectedRoute
|
||||||
|
path="/profile"
|
||||||
|
component={ProfilePage}
|
||||||
exact
|
exact
|
||||||
/>
|
/>
|
||||||
<Redirect exact from="/" to="/home" />
|
|
||||||
</IonRouterOutlet>
|
</IonRouterOutlet>
|
||||||
</AuthProvider>
|
<IonTabBar slot="bottom">
|
||||||
|
<IonTabButton tab="icebreaker" href="/icebreaker">
|
||||||
|
<IonIcon icon={flameOutline} />
|
||||||
|
<IonLabel>Icebreaker</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
<IonTabButton tab="hangout" href="/hangout">
|
||||||
|
<IonIcon icon={locationOutline} />
|
||||||
|
<IonLabel>Hangout</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
<IonTabButton tab="chats" href="/chats">
|
||||||
|
<IonIcon icon={chatbubblesOutline} />
|
||||||
|
<IonLabel>Chats</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
<IonTabButton tab="profile" href="/profile">
|
||||||
|
<IonIcon icon={personOutline} />
|
||||||
|
<IonLabel>Profile</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
</IonTabBar>
|
||||||
|
</IonTabs>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/"
|
||||||
|
render={() => (
|
||||||
|
<Redirect to={isPreviewMode() ? '/icebreaker' : '/auth'} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</IonRouterOutlet>
|
||||||
</IonReactRouter>
|
</IonReactRouter>
|
||||||
|
</AuthProvider>
|
||||||
</IonApp>
|
</IonApp>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
|
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 |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1 @@
|
|||||||
|
{"nm":"Main Scene","ddd":0,"h":1920,"w":1920,"meta":{"g":"@lottiefiles/creator 1.37.1"},"layers":[{"ty":4,"nm":"Layer 1 Outlines 3","sr":1,"st":0,"op":420,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[523.115,1227.748,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[43,36],"t":0},{"s":[157,192.00000000000003],"t":25}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[960,1277.507],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[30],"t":12},{"s":[0],"t":25}],"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 3","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-265.484,0],[0,-135.236],[265.484,0],[0,135.235]],"o":[[265.484,0],[0,135.235],[-265.484,0],[0,-135.236]],"v":[[0,-244.865],[480.701,0.001],[0,244.866],[-480.701,0.001]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[20],"t":0},{"s":[50],"t":25}],"ix":5},"c":{"a":0,"k":[0.9961,0.2353,0.4471],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[523.115,1227.748],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Layer 1 Outlines 2","sr":1,"st":0,"op":420,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[523.115,382.842,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[960,413.401],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[960,433.401],"t":12},{"s":[960,413.401],"t":25}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":6,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[106.139,0],[0,106.14],[-106.139,0],[0,-106.141]],"o":[[-106.141,0],[0,-106.141],[106.139,0],[0,106.141]],"v":[[0,44.966],[-192.49,-147.526],[0,-340.016],[192.488,-147.524]]},"ix":2}},{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 2","ix":2,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[210.961,0],[0,-210.961],[-14.577,-16.234],[-13.668,15.223],[0,261.809]],"o":[[-210.963,0],[0,261.809],[13.693,15.25],[14.577,-16.234],[-0.004,-210.961]],"v":[[0,-530.117],[-382.593,-147.526],[-25.633,514.867],[25.633,514.867],[382.593,-147.526]]},"ix":2}},{"ty":"mm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Merge","nm":"Merge Paths 1","mm":1},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.9961,0.2353,0.4471],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[523.115,530.367],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2},{"ty":4,"nm":"Layer 1 Outlines","sr":1,"st":0,"op":420,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[523.115,1307.748],"ix":1},"s":{"a":0,"k":[100,123],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[960,1278.307],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":80,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 2","ix":1,"cix":2,"np":4,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,-46.929],[123.812,0],[0,46.929],[-123.812,0]],"o":[[0,46.929],[-123.812,0],[0,-46.929],[123.812,0]],"v":[[224.182,0],[0,84.972],[-224.182,0],[0,-84.972]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.9961,0.2353,0.4471],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[523.115,1307.748],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3}],"v":"5.7.0","fr":30,"op":30,"ip":0,"assets":[]}
|
||||||
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
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,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { getStorageUrl, getInitials, getInitialsColor } from '../utils/storage';
|
||||||
|
|
||||||
|
interface AvatarProps {
|
||||||
|
src?: string | null;
|
||||||
|
name?: string | null;
|
||||||
|
size?: number;
|
||||||
|
bucket?: string;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Avatar: React.FC<AvatarProps> = ({
|
||||||
|
src,
|
||||||
|
name,
|
||||||
|
size = 48,
|
||||||
|
bucket = 'avatars',
|
||||||
|
className = '',
|
||||||
|
style = {},
|
||||||
|
}) => {
|
||||||
|
const url =
|
||||||
|
src?.startsWith('/src/assets/') || src?.startsWith('/assets/')
|
||||||
|
? src
|
||||||
|
: getStorageUrl(bucket, src);
|
||||||
|
|
||||||
|
const containerStyle: React.CSSProperties = {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: '50%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
background: url ? 'transparent' : getInitialsColor(name || null),
|
||||||
|
...style,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
return (
|
||||||
|
<div className={className} style={containerStyle}>
|
||||||
|
<img
|
||||||
|
src={url}
|
||||||
|
alt={name || 'Avatar'}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
onError={(e) => {
|
||||||
|
(e.target as HTMLImageElement).style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} style={containerStyle}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: size * 0.4,
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getInitials(name || null)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Avatar;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { IonButton, IonIcon } from '@ionic/react';
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
ctaText?: string;
|
||||||
|
onCtaClick?: () => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmptyState: React.FC<EmptyStateProps> = ({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
ctaText,
|
||||||
|
onCtaClick,
|
||||||
|
style = {},
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '40px 20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
minHeight: '300px',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '80px',
|
||||||
|
height: '80px',
|
||||||
|
borderRadius: '24px',
|
||||||
|
background: 'var(--color-surface-raised)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IonIcon
|
||||||
|
icon={icon}
|
||||||
|
style={{ fontSize: '40px', color: 'var(--ion-color-primary)' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
style={{
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: 700,
|
||||||
|
margin: '0 0 8px',
|
||||||
|
color: 'var(--color-text-primary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: '15px',
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
margin: '0 0 24px',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
{ctaText && onCtaClick && (
|
||||||
|
<IonButton
|
||||||
|
expand="block"
|
||||||
|
onClick={onCtaClick}
|
||||||
|
style={{ '--border-radius': 'var(--radius-pill)', minWidth: '200px' }}
|
||||||
|
>
|
||||||
|
{ctaText}
|
||||||
|
</IonButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptyState;
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface OtpInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
length?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OtpInput: React.FC<OtpInputProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
length = 6,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
index: number
|
||||||
|
) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (!val) return;
|
||||||
|
|
||||||
|
const newOtp = value.split('');
|
||||||
|
newOtp[index] = val.slice(-1);
|
||||||
|
const combined = newOtp.join('');
|
||||||
|
onChange(combined);
|
||||||
|
|
||||||
|
if (val && index < length - 1) {
|
||||||
|
inputRefs.current[index + 1]?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (
|
||||||
|
e: React.KeyboardEvent<HTMLInputElement>,
|
||||||
|
index: number
|
||||||
|
) => {
|
||||||
|
if (e.key === 'Backspace' && !value[index] && index > 0) {
|
||||||
|
inputRefs.current[index - 1]?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaste = (e: React.ClipboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const pastedData = e.clipboardData.getData('text').slice(0, length);
|
||||||
|
if (/^\d+$/.test(pastedData)) {
|
||||||
|
onChange(pastedData);
|
||||||
|
inputRefs.current[Math.min(pastedData.length, length - 1)]?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '24px 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({ length }).map((_, i) => (
|
||||||
|
<input
|
||||||
|
key={i}
|
||||||
|
ref={(el) => {
|
||||||
|
inputRefs.current[i] = el;
|
||||||
|
}}
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="\d*"
|
||||||
|
maxLength={1}
|
||||||
|
value={value[i] || ''}
|
||||||
|
onChange={(e) => handleChange(e, i)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, i)}
|
||||||
|
onPaste={handlePaste}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{
|
||||||
|
width: '44px',
|
||||||
|
height: '52px',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '22px',
|
||||||
|
fontWeight: 700,
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: '2px solid transparent',
|
||||||
|
background: 'var(--color-surface)',
|
||||||
|
color: 'var(--color-text-primary)',
|
||||||
|
outline: 'none',
|
||||||
|
transition: 'border-color 0.2s',
|
||||||
|
...(value[i] ? { borderColor: 'var(--ion-color-primary)' } : {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</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;
|
|
||||||
@@ -1,30 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, RouteProps } from 'react-router-dom';
|
import { Route, Redirect, RouteProps } from 'react-router-dom';
|
||||||
import { IonLoading } from '@ionic/react';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { isPreviewMode } from '../utils/previewMode';
|
||||||
|
|
||||||
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
|
||||||
if (!Component) return null;
|
}) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const previewMode = isPreviewMode();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
{...rest}
|
{...rest}
|
||||||
render={(props) => {
|
render={(props) =>
|
||||||
if (isLoading) {
|
user || previewMode ? <Component {...props} /> : <Redirect to="/auth" />
|
||||||
return <IonLoading isOpen message="Loading..." />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return <Redirect to="/auth" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RouteComponent = Component as React.ComponentType<any>;
|
|
||||||
return <RouteComponent {...props} />;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { IonSearchbar, IonButton, IonIcon } from '@ionic/react';
|
||||||
|
import { optionsOutline } from 'ionicons/icons';
|
||||||
|
|
||||||
|
interface SearchRowProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (e: any) => void;
|
||||||
|
onFilterClick?: () => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchRow: React.FC<SearchRowProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onFilterClick,
|
||||||
|
placeholder = 'Search...',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
margin: '0 20px 14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IonSearchbar
|
||||||
|
value={value}
|
||||||
|
onIonInput={onChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
style={{
|
||||||
|
padding: 0,
|
||||||
|
'--border-radius': 'var(--radius-pill)',
|
||||||
|
'--background': 'rgba(255,255,255,0.06)',
|
||||||
|
'--color': 'var(--color-text-primary)',
|
||||||
|
'--placeholder-color': 'rgba(255,255,255,0.45)',
|
||||||
|
'--icon-color': 'rgba(255,255,255,0.75)',
|
||||||
|
'--clear-button-color': 'rgba(255,255,255,0.55)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{onFilterClick && (
|
||||||
|
<IonButton
|
||||||
|
onClick={onFilterClick}
|
||||||
|
style={{
|
||||||
|
'--background': 'rgba(255,255,255,0.06)',
|
||||||
|
'--color': 'var(--color-text-primary)',
|
||||||
|
'--border-radius': '12px',
|
||||||
|
width: '44px',
|
||||||
|
height: '44px',
|
||||||
|
margin: 0,
|
||||||
|
flexShrink: 0,
|
||||||
|
'--padding-start': 0,
|
||||||
|
'--padding-end': 0,
|
||||||
|
'--box-shadow': 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IonIcon
|
||||||
|
icon={optionsOutline}
|
||||||
|
slot="icon-only"
|
||||||
|
style={{ fontSize: '20px' }}
|
||||||
|
/>
|
||||||
|
</IonButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchRow;
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||