Compare commits

..

1 Commits

Author SHA1 Message Date
AppCakes 0119c17e34 build: 91279040-7fca-48e0-9d50-5ce29aae63f6 2026-06-19 06:45:54 +00:00
145 changed files with 7298 additions and 8724 deletions
Vendored
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -4,7 +4,7 @@ android {
namespace = "io.ionic.starter"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "io.ionic.starter"
applicationId "com.inspot.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
+17
View File
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<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
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
@@ -21,7 +26,19 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</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>
<meta-data
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+2 -2
View File
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">bringg</string>
<string name="title_activity_main">bringg</string>
<string name="app_name">InSpot</string>
<string name="title_activity_main">InSpot</string>
<string name="package_name">io.ionic.starter</string>
<string name="custom_url_scheme">io.ionic.starter</string>
</resources>
-1
View File
@@ -15,5 +15,4 @@ ext {
cordovaAndroidVersion = '14.0.1'
rgcfaIncludeGoogle = true
androidxCredentialsVersion = '1.3.0'
javaVersion = JavaVersion.VERSION_17
}
+2 -2
View File
@@ -1,8 +1,8 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.bringg.app',
appName: 'bringg',
appId: 'com.inspot.app',
appName: 'InSpot',
webDir: 'dist',
plugins: {
FirebaseAuthentication: {
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+6 -2
View File
@@ -26,6 +26,7 @@
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>"; };
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>"; };
958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */
@@ -68,6 +69,7 @@
504EC30E1FED79650016851F /* Assets.xcassets */,
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
504EC3131FED79650016851F /* Info.plist */,
1A4EB3C21FED79650016851F /* App.entitlements */,
2FAD9762203C412B000D30F8 /* config.xml */,
50B271D01FEDC1A000F3C39B /* public */,
);
@@ -295,6 +297,7 @@
baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = App/Info.plist;
@@ -305,7 +308,7 @@
);
MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.bringg.app;
PRODUCT_BUNDLE_IDENTIFIER = com.inspot.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
@@ -317,6 +320,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = App/Info.plist;
@@ -326,7 +330,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.bringg.app;
PRODUCT_BUNDLE_IDENTIFIER = com.inspot.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;
+18 -1
View File
@@ -1,5 +1,7 @@
import UIKit
import Capacitor
import FirebaseCore
import FirebaseMessaging
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -7,10 +9,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
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) {
// 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.
@@ -1,14 +1,122 @@
{
"images" : [
"images": [
{
"filename" : "AppIcon-512@2x.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
"idiom": "iphone",
"scale": "1x",
"size": "20x20",
"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" : {
"author" : "xcode",
"version" : 1
"info": {
"author": "appsuite",
"version": 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+22 -3
View File
@@ -7,7 +7,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>bringg</string>
<string>InSpot</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -51,12 +51,31 @@
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.bringg.app</string>
<string>com.inspot.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.bringg.app</string>
<string>com.inspot.app</string>
</array>
</dict>
</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>
</plist>
+5 -3
View File
@@ -20,13 +20,15 @@
"@capacitor/keyboard": "8.0.3",
"@capacitor/push-notifications": "^8.1.1",
"@capacitor/status-bar": "8.0.2",
"@hookform/resolvers": "^3.9.0",
"@ionic/pwa-elements": "^3.0.0",
"@ionic/react": "^8.5.0",
"@ionic/react-router": "^8.5.0",
"@supabase/supabase-js": "^2.0.0",
"@ionic/react": "8.8.7",
"@ionic/react-router": "8.8.7",
"@supabase/supabase-js": "^2.108.1",
"firebase": "^11.0.0",
"ionicons": "^7.4.0",
"katex": "^0.16.0",
"lottie-react": "^2.4.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.54.0",
BIN
View File
Binary file not shown.
+119 -39
View File
@@ -1,12 +1,29 @@
import React from 'react';
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 {
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/normalize.css';
import '@ionic/react/css/structure.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/float-elements.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/display.css';
/* Theme variables */
import './theme/variables.css';
import Home from './pages/Home';
import ProductDetail from './pages/ProductDetail';
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';
/* Pages */
import AuthPage from './pages/AuthPage';
import VerifyEmailPage from './pages/VerifyEmailPage';
import ForgotPasswordPage from './pages/ForgotPasswordPage';
import NewPasswordPage from './pages/NewPasswordPage';
import OnboardingProfilePage from './pages/OnboardingProfilePage';
import IcebreakerPage from './pages/IcebreakerPage';
import HangoutPage from './pages/HangoutPage';
import ReelsPage from './pages/ReelsPage';
import CameraPage from './pages/CameraPage';
import ChatsPage from './pages/ChatsPage';
import ChatThreadPage from './pages/ChatThreadPage';
import ProfilePage from './pages/ProfilePage';
import EditProfilePage from './pages/EditProfilePage';
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 = () => (
<IonApp>
<IonReactRouter>
<AuthProvider>
<IonReactRouter>
<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}
{/* Auth Stack */}
<Route path="/auth" component={AuthPage} exact />
<Route path="/verify-email" component={VerifyEmailPage} exact />
<Route path="/forgot-password" component={ForgotPasswordPage} exact />
<Route path="/new-password" component={NewPasswordPage} exact />
<ProtectedRoute
path="/onboarding/profile"
component={OnboardingProfilePage}
exact
/>
<Route
path="/wallet/payment-methods/add"
component={AddPaymentMethod}
{/* Detailed Stack (No Tabs) */}
<ProtectedRoute path="/reels/:placeId" component={ReelsPage} exact />
<ProtectedRoute
path="/chat/:conversationId"
component={ChatThreadPage}
exact
/>
<Route path="/admin/orders" component={AdminOrders} exact />
<Route path="/admin/orders/:id" component={AdminOrderDetail} exact />
<Route
path="/order-confirmation"
component={OrderConfirmation}
<ProtectedRoute
path="/profile/edit"
component={EditProfilePage}
exact
/>
<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
/>
<Redirect exact from="/" to="/home" />
</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>
</AuthProvider>
</IonApp>
);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 KiB

View File
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 KiB

View File
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+1
View File
@@ -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":[]}
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 690 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 697 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 KiB

-36
View File
@@ -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;
+70
View File
@@ -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;
+84
View File
@@ -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;
+97
View File
@@ -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;
-101
View File
@@ -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;
+14 -19
View File
@@ -1,30 +1,25 @@
import React from 'react';
import { Redirect, Route, RouteProps } from 'react-router-dom';
import { IonLoading } from '@ionic/react';
import { useAuth } from '../context/AuthContext';
import { Route, Redirect, RouteProps } from 'react-router-dom';
import { useAuth } from '../contexts/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 { user, isLoading } = useAuth();
if (!Component) return null;
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
component: Component,
...rest
}) => {
const { user } = useAuth();
const previewMode = isPreviewMode();
return (
<Route
{...rest}
render={(props) => {
if (isLoading) {
return <IonLoading isOpen message="Loading..." />;
render={(props) =>
user || previewMode ? <Component {...props} /> : <Redirect to="/auth" />
}
if (!user) {
return <Redirect to="/auth" />;
}
const RouteComponent = Component as React.ComponentType<any>;
return <RouteComponent {...props} />;
}}
/>
);
};
+68
View File
@@ -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;
-97
View File
@@ -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;
};

Some files were not shown because too many files have changed in this diff Show More