Compare commits
211 Commits
tablet
...
7a7b1902a8
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a7b1902a8 | |||
| c184eb3293 | |||
| d9ee1cd921 | |||
| 1b5024089d | |||
| 59664488e8 | |||
| bb1fb8d8f7 | |||
| b804f37037 | |||
| f649828d80 | |||
| 718fd562ff | |||
| a855e6d3bf | |||
| e04441bd81 | |||
| f9a5e76162 | |||
| 479e7c5f56 | |||
| a8957c7ac7 | |||
| 3d15d7bb74 | |||
| 6ada9470c3 | |||
| 300ce82a4d | |||
| 5ac4526baf | |||
| 5cfec1544a | |||
| e96210986e | |||
| 0998dc29d0 | |||
| dc7d59eecc | |||
| 14be80c6f0 | |||
| 580104d052 | |||
| ba3ea5a6d2 | |||
| 231e99ff8f | |||
| 5333b8cbb7 | |||
| dcca652019 | |||
| 413164128b | |||
| bc77c403f2 | |||
| 1d4903b609 | |||
| 9c3b9b3663 | |||
| 60953c34bc | |||
| adafaf1dfe | |||
| 0c1bed62d9 | |||
| 5b7f54403c | |||
| 81419c0a23 | |||
| 74c0369acf | |||
| 0f07e46a82 | |||
| a4e27f9dc8 | |||
| 6cfb33e227 | |||
| 2a6357983f | |||
| 04d865cce9 | |||
| f7dd31cae9 | |||
| 141337d2ce | |||
| ffe8cc72a1 | |||
| 239a08d4aa | |||
| 49be903553 | |||
| 8b6171a819 | |||
| 7b809d826c | |||
| afa1046d02 | |||
| ff81570e15 | |||
| 81d791dd17 | |||
| 42b18856e4 | |||
| ab7a844bb5 | |||
| 28110b88cc | |||
| f1869f02f2 | |||
| c14910447e | |||
| 7d3e39b77d | |||
| 406f541163 | |||
| 609d01b81c | |||
| 10f6616cd0 | |||
| 5c6915c23d | |||
| fb9be26e04 | |||
| 2c6474007c | |||
| 4866dd1b3f | |||
| c93d66d13d | |||
| 1c9f9f865c | |||
| 00092ae5f8 | |||
| 6972066f91 | |||
| 63699d53f4 | |||
| e86b2a5950 | |||
| 1903e4072c | |||
| 8aca8e46db | |||
| b7fd8daddf | |||
| 8b802f492d | |||
| 3d96393972 | |||
| 956628bcb6 | |||
| 0b56b169a5 | |||
| 4b67033aa8 | |||
| 3324a2fd6f | |||
| 79ac1efe63 | |||
| 32a718953b | |||
| 3564edadf2 | |||
| f7860a5af8 | |||
| 8c28f049f7 | |||
| 003f3717e5 | |||
| 0733c25892 | |||
| 335905335e | |||
| a4bd7920e4 | |||
| 5be4e15885 | |||
| 72aa06ad22 | |||
| 56f37cef94 | |||
| 035f8af84b | |||
| bb68a89cfb | |||
| 818107efe3 | |||
| b59a8bf97d | |||
| 5cafd10f09 | |||
| c411990312 | |||
| 70db8bdc0b | |||
| a6009beb03 | |||
| 0aec9c1ee8 | |||
| 4f96bb071b | |||
| 67c896b9c6 | |||
| d30ed9b130 | |||
| 5bb250f7ea | |||
| a305aedeeb | |||
| fe9ee5334d | |||
| 35608e350f | |||
| 233427bf38 | |||
| 2740907d63 | |||
| ee04ba7850 | |||
| ffb0df6bac | |||
| 3fe46f1954 | |||
| f382d403cd | |||
| 1c2f18e288 | |||
| 47a0ea2f36 | |||
| 12f1a51292 | |||
| ecee34bf26 | |||
| 33ada3505b | |||
| 89bb924b40 | |||
| 94784b3706 | |||
| 11115d151d | |||
| 9700a6b269 | |||
| 5866004f6a | |||
| a17e0f1e22 | |||
| 8526d79ba1 | |||
| 3880b0e1f4 | |||
| 771dba9658 | |||
| 1b288d095f | |||
| c86a355b97 | |||
| 12b4ce3a70 | |||
| 92e879c3fc | |||
| dfe7301f6d | |||
| 01b4fc2e33 | |||
| 3dff040cea | |||
| 95d5e74703 | |||
| f2af60111b | |||
| 5cfdc84055 | |||
| 4b5900c652 | |||
| ee749e1077 | |||
| cd178b8a9d | |||
| d091aaa0ee | |||
| 06a3a2dc8f | |||
| 29ec985ffa | |||
| fb27079f10 | |||
| 726668921b | |||
| c528f72b2e | |||
| f74a6390a2 | |||
| edea14b32c | |||
| 4f80525f13 | |||
| 830c1bf8ea | |||
| b5aac0af2a | |||
| b1a5d4c171 | |||
| e113d78575 | |||
| 012a70313b | |||
| cc2a649f8c | |||
| 81477ed85b | |||
| 9f32e81672 | |||
| bd890406b1 | |||
| 47f035708c | |||
| 8763be4613 | |||
| 89d00cdead | |||
| d8e7dc6bf7 | |||
| 0ba2cbd65d | |||
| 662b38f894 | |||
| f269c9c136 | |||
| eace5da5cb | |||
| 55d3877c5b | |||
| 111244e178 | |||
| c9b796bd40 | |||
| c1f7113dfc | |||
| f21f70a45d | |||
| 9d16e8b301 | |||
| d7928c799d | |||
| 4fa18a6913 | |||
| a95191c890 | |||
| 6489bdf237 | |||
| 3fb9dd0035 | |||
| e2aae47c34 | |||
| 5b80a3ba80 | |||
| 7da8005fb1 | |||
| f7909de546 | |||
| 844f534e13 | |||
| 8ac7f8b2eb | |||
| 4318f58e22 | |||
| 84a974f3f7 | |||
| 848211c3c8 | |||
| a577bfb23f | |||
| f1b0bcd32d | |||
| 8a0370933d | |||
| a6b2c4b97f | |||
| e25a4f07ff | |||
| c4e4a4a798 | |||
| 18223c5f92 | |||
| 87137e7b15 | |||
| 61fff87975 | |||
| e306f312f4 | |||
| 5e82fffa80 | |||
| 3e517e3a53 | |||
| a4a0a66a2a | |||
| 9f37d22d5a | |||
| c1252cf092 | |||
| e965072186 | |||
| 0a54d1e576 | |||
| 38586d34ff | |||
| 9ca96b2286 | |||
| 6e7a3475d1 | |||
| 0d2fe33caa | |||
| 4f1a96d0f1 | |||
| 2605f4622b |
6
.github/workflows/ci-cd.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
cache: yarn
|
||||
|
||||
- name: Setup Expo and EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
@ -34,7 +34,7 @@ jobs:
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Prebuild, Build and Submit
|
||||
run: npm run prebuild-build-submit-ios-cicd
|
||||
run: yarn prebuild-build-submit-ios-cicd
|
||||
1
.gitignore
vendored
@ -21,3 +21,4 @@ expo-env.d.ts
|
||||
/ios/GoogleService-Info.plist
|
||||
/ios/cally.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
|
||||
expo-env.d.ts
|
||||
./android
|
||||
|
||||
6
.idea/git_toolbox_blame.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxBlameSettings">
|
||||
<option name="version" value="2" />
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="t" enabled="true" level="TEXT ATTRIBUTES" enabled_by_default="true" editorAttributes="CONSIDERATION_ATTRIBUTES" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/jsLinters/eslint.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EslintConfiguration">
|
||||
<option name="fix-on-save" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
85
.idea/jsLinters/jshint.xml
generated
Normal file
@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JSHintConfiguration" version="2.13.6" use-config-file="false">
|
||||
<option asi="false" />
|
||||
<option bitwise="true" />
|
||||
<option boss="false" />
|
||||
<option browser="true" />
|
||||
<option browserify="false" />
|
||||
<option camelcase="true" />
|
||||
<option couch="false" />
|
||||
<option curly="true" />
|
||||
<option debug="false" />
|
||||
<option devel="false" />
|
||||
<option dojo="false" />
|
||||
<option elision="false" />
|
||||
<option enforceall="false" />
|
||||
<option eqeqeq="true" />
|
||||
<option eqnull="false" />
|
||||
<option es3="false" />
|
||||
<option es5="false" />
|
||||
<option esnext="false" />
|
||||
<option evil="false" />
|
||||
<option expr="false" />
|
||||
<option forin="true" />
|
||||
<option freeze="true" />
|
||||
<option funcscope="false" />
|
||||
<option futurehostile="false" />
|
||||
<option gcl="false" />
|
||||
<option globalstrict="false" />
|
||||
<option immed="false" />
|
||||
<option iterator="false" />
|
||||
<option jasmine="false" />
|
||||
<option jquery="false" />
|
||||
<option lastsemic="false" />
|
||||
<option latedef="false" />
|
||||
<option laxbreak="false" />
|
||||
<option laxcomma="false" />
|
||||
<option loopfunc="false" />
|
||||
<option maxerr="50" />
|
||||
<option mocha="false" />
|
||||
<option module="false" />
|
||||
<option mootools="false" />
|
||||
<option moz="false" />
|
||||
<option multistr="false" />
|
||||
<option newcap="false" />
|
||||
<option noarg="true" />
|
||||
<option nocomma="false" />
|
||||
<option node="false" />
|
||||
<option noempty="true" />
|
||||
<option nomen="false" />
|
||||
<option nonbsp="false" />
|
||||
<option nonew="true" />
|
||||
<option nonstandard="false" />
|
||||
<option notypeof="false" />
|
||||
<option noyield="false" />
|
||||
<option onevar="false" />
|
||||
<option passfail="false" />
|
||||
<option phantom="false" />
|
||||
<option plusplus="false" />
|
||||
<option proto="false" />
|
||||
<option prototypejs="false" />
|
||||
<option qunit="false" />
|
||||
<option quotmark="false" />
|
||||
<option rhino="false" />
|
||||
<option scripturl="false" />
|
||||
<option shadow="false" />
|
||||
<option shelljs="false" />
|
||||
<option singleGroups="false" />
|
||||
<option smarttabs="false" />
|
||||
<option strict="true" />
|
||||
<option sub="false" />
|
||||
<option supernew="false" />
|
||||
<option trailing="false" />
|
||||
<option typed="false" />
|
||||
<option undef="true" />
|
||||
<option unused="false" />
|
||||
<option validthis="false" />
|
||||
<option varstmt="false" />
|
||||
<option white="false" />
|
||||
<option withstmt="false" />
|
||||
<option worker="false" />
|
||||
<option wsh="false" />
|
||||
<option yui="false" />
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/material_theme_project_new.xml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MaterialThemeProjectNewConfig">
|
||||
<option name="metadata">
|
||||
<MTProjectMetadataState>
|
||||
<option name="migrated" value="true" />
|
||||
<option name="pristineConfig" value="false" />
|
||||
<option name="userId" value="-7d3b2185:193a8bd7023:-7ffe" />
|
||||
</MTProjectMetadataState>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@ -1,5 +1,4 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
@ -21,12 +20,12 @@ react {
|
||||
bundleCommand = "export:embed"
|
||||
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '..'
|
||||
// root = file("../")
|
||||
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
|
||||
// reactNativeDir = file("../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../node_modules/@react-native/codegen")
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
@ -58,6 +57,9 @@ react {
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,9 +93,6 @@ android {
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
manifestPlaceholders = [
|
||||
appAuthRedirectScheme: "callyplanner"
|
||||
]
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
@ -122,6 +121,9 @@ android {
|
||||
useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
|
||||
}
|
||||
}
|
||||
androidResources {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
|
||||
// Apply static values from `gradle.properties` to the `android.packagingOptions`
|
||||
@ -173,7 +175,5 @@ dependencies {
|
||||
}
|
||||
}
|
||||
|
||||
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
|
||||
applyNativeModulesAppBuildGradle(project)
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
@ -15,7 +15,7 @@
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true">
|
||||
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="default"/>
|
||||
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color"/>
|
||||
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
|
||||
@ -33,12 +33,10 @@
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="myapp"/>
|
||||
<data android:scheme="callyplanner"/>
|
||||
<data android:scheme="com.cally.app"/>
|
||||
<data android:scheme="exp+cally"/>
|
||||
<data android:scheme="callyplanner"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false"/>
|
||||
</application>
|
||||
</manifest>
|
||||
@ -1,4 +1,5 @@
|
||||
package com.cally.app
|
||||
import expo.modules.splashscreen.SplashScreenManager
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@ -15,7 +16,10 @@ class MainActivity : ReactActivity() {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
setTheme(R.style.AppTheme);
|
||||
// setTheme(R.style.AppTheme);
|
||||
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
|
||||
SplashScreenManager.registerOnActivity(this)
|
||||
// @generated end expo-splashscreen
|
||||
super.onCreate(null)
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
@ -21,9 +22,10 @@ class MainApplication : Application(), ReactApplication {
|
||||
this,
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> {
|
||||
val packages = PackageList(this).packages
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
return PackageList(this).packages
|
||||
return packages
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
||||
@ -40,7 +42,7 @@ class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
SoLoader.init(this, false)
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
|
||||
BIN
android/app/src/main/res/drawable-hdpi/splashscreen_logo.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splashscreen_logo.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 29 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 29 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 29 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
@ -1,3 +1,6 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/splashscreen_background"/>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
Before Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 16 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 23 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 25 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 46 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 50 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 72 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 77 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
@ -1,6 +1,6 @@
|
||||
<resources>
|
||||
<color name="splashscreen_background">#ffffff</color>
|
||||
<color name="iconBackground">#ffffff</color>
|
||||
<color name="iconBackground">#FFFFFF</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
<color name="colorPrimaryDark">#ffffff</color>
|
||||
<color name="notification_icon_color">#ffffff</color>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<resources>
|
||||
<string name="app_name">"Cally "</string>
|
||||
<string name="app_name">\"Cally \"</string>
|
||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>
|
||||
|
||||
@ -11,7 +11,9 @@
|
||||
<item name="android:textColorHint">#c8c8c8</item>
|
||||
<item name="android:textColor">@android:color/black</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||
<item name="android:windowBackground">@drawable/splashscreen</item>
|
||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
</resources>
|
||||
@ -2,11 +2,11 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0'
|
||||
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23')
|
||||
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '34')
|
||||
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'
|
||||
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
|
||||
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
|
||||
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
|
||||
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.23'
|
||||
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.24'
|
||||
|
||||
ndkVersion = "26.1.10909125"
|
||||
}
|
||||
@ -16,10 +16,10 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.2'
|
||||
classpath 'com.google.gms:google-services:4.4.1'
|
||||
classpath('com.android.tools.build:gradle')
|
||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||
classpath('com.google.gms:google-services:4.4.2')
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +41,3 @@ allprojects {
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
// @generated begin expo-camera-import - expo prebuild (DO NOT MODIFY) sync-f244f4f3d8bf7229102e8f992b525b8602c74770
|
||||
def expoCameraMavenPath = new File(["node", "--print", "require.resolve('expo-camera/package.json')"].execute(null, rootDir).text.trim(), "../android/maven")
|
||||
allprojects { repositories { maven { url(expoCameraMavenPath) } } }
|
||||
// @generated end expo-camera-import
|
||||
@ -22,9 +22,6 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
# Enable AAPT2 PNG crunching
|
||||
android.enablePngCrunchInReleaseBuilds=true
|
||||
|
||||
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
7
android/gradlew
vendored
Executable file → Normal file
@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@ -84,7 +86,8 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
2
android/gradlew.bat
vendored
@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
|
||||
@ -1,3 +1,26 @@
|
||||
pluginManagement {
|
||||
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile().toString())
|
||||
}
|
||||
plugins { id("com.facebook.react.settings") }
|
||||
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
||||
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
|
||||
ex.autolinkLibrariesFromCommand()
|
||||
} else {
|
||||
def command = [
|
||||
'node',
|
||||
'--no-warnings',
|
||||
'--eval',
|
||||
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
|
||||
'react-native-config',
|
||||
'--json',
|
||||
'--platform',
|
||||
'android'
|
||||
].toList()
|
||||
ex.autolinkLibrariesFromCommand(command)
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = 'Cally '
|
||||
|
||||
dependencyResolutionManagement {
|
||||
@ -11,8 +34,5 @@ dependencyResolutionManagement {
|
||||
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
|
||||
useExpoModules()
|
||||
|
||||
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
|
||||
applyNativeModulesSettingsGradle(settings)
|
||||
|
||||
include ':app'
|
||||
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile())
|
||||
|
||||
12
app.json
@ -4,6 +4,7 @@
|
||||
"slug": "cally",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"owner": "tomira",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "callyplanner",
|
||||
"userInterfaceStyle": "light",
|
||||
@ -16,16 +17,19 @@
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.cally.app",
|
||||
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
||||
"buildNumber": "60",
|
||||
"usesAppleSignIn": true
|
||||
"buildNumber": "100",
|
||||
"usesAppleSignIn": true,
|
||||
"infoPlist": {
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
"backgroundColor": "#FFFFFF"
|
||||
},
|
||||
"package": "com.cally.app",
|
||||
"googleServicesFile": "./android/app/google-services.json",
|
||||
"googleServicesFile": "./google-services.json",
|
||||
"permissions": [
|
||||
"android.permission.CAMERA",
|
||||
"android.permission.RECORD_AUDIO"
|
||||
|
||||
@ -1,286 +1,362 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Drawer } from "expo-router/drawer";
|
||||
import { useSignOut } from "@/hooks/firebase/useSignOut";
|
||||
import React, {memo, useCallback, useMemo} from "react";
|
||||
import {Drawer} from "expo-router/drawer";
|
||||
import {
|
||||
DrawerContentScrollView,
|
||||
DrawerItem,
|
||||
DrawerItemList,
|
||||
DrawerContentComponentProps,
|
||||
DrawerContentScrollView,
|
||||
DrawerNavigationOptions,
|
||||
DrawerNavigationProp
|
||||
} from "@react-navigation/drawer";
|
||||
import { Button, View, Text, ButtonSize } from "react-native-ui-lib";
|
||||
import { Dimensions, ImageBackground, StyleSheet } from "react-native";
|
||||
import Feather from "@expo/vector-icons/Feather";
|
||||
import {ImageBackground, Pressable, StyleSheet} from "react-native";
|
||||
import {Button, ButtonSize, Text, View} from "react-native-ui-lib";
|
||||
import * as Device from "expo-device";
|
||||
import {DeviceType} from "expo-device";
|
||||
import {useSetAtom} from "jotai";
|
||||
import {Ionicons} from "@expo/vector-icons";
|
||||
import {ParamListBase, RouteProp, Theme} from '@react-navigation/native';
|
||||
|
||||
import {useSignOut} from "@/hooks/firebase/useSignOut";
|
||||
import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader";
|
||||
import DrawerButton from "@/components/shared/DrawerButton";
|
||||
import {
|
||||
AntDesign,
|
||||
FontAwesome6,
|
||||
MaterialCommunityIcons,
|
||||
Octicons,
|
||||
} from "@expo/vector-icons";
|
||||
import MenuIcon from "@/assets/svgs/MenuIcon";
|
||||
import { router } from "expo-router";
|
||||
import DrawerIcon from "@/assets/svgs/DrawerIcon";
|
||||
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
|
||||
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||
import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
|
||||
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
||||
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
|
||||
import ViewSwitch from "@/components/pages/(tablet_pages)/ViewSwitch";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import {
|
||||
isFamilyViewAtom,
|
||||
settingsPageIndex,
|
||||
toDosPageIndex,
|
||||
userSettingsView,
|
||||
} from "@/components/pages/calendar/atoms";
|
||||
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
|
||||
import Constants from "expo-constants";
|
||||
import * as Device from "expo-device";
|
||||
import { DeviceType } from "expo-device";
|
||||
import {
|
||||
isFamilyViewAtom,
|
||||
settingsPageIndex,
|
||||
toDosPageIndex,
|
||||
userSettingsView,
|
||||
} from "@/components/pages/calendar/atoms";
|
||||
import ViewSwitch from "@/components/pages/(tablet_pages)/ViewSwitch";
|
||||
|
||||
export default function TabLayout() {
|
||||
const { mutateAsync: signOut } = useSignOut();
|
||||
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
||||
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||
const setUserView = useSetAtom(userSettingsView);
|
||||
const setToDosIndex = useSetAtom(toDosPageIndex);
|
||||
const isTablet = Device.deviceType === DeviceType.TABLET;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
initialRouteName={"index"}
|
||||
detachInactiveScreens
|
||||
screenOptions={({ navigation }) => ({
|
||||
headerShown: true,
|
||||
headerRight: () =>
|
||||
Device.deviceType === DeviceType.TABLET ? (
|
||||
<ViewSwitch navigation={navigation} />
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
drawerStyle: {
|
||||
width: "90%",
|
||||
backgroundColor: "#f9f8f7",
|
||||
height: "100%",
|
||||
},
|
||||
})}
|
||||
drawerContent={(props) => {
|
||||
return (
|
||||
<DrawerContentScrollView {...props} style={{}}>
|
||||
<View centerV marginH-30 marginT-20 marginB-20 row>
|
||||
<ImageBackground
|
||||
source={require("../../assets/images/splash.png")}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
height: 51.43,
|
||||
aspectRatio: 1,
|
||||
marginRight: 8,
|
||||
}}
|
||||
/>
|
||||
<Text style={styles.title}>Welcome to Cally</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 30,
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, paddingRight: 5 }}>
|
||||
<DrawerButton
|
||||
title={"Calendar"}
|
||||
color="rgb(7, 184, 199)"
|
||||
bgColor={"rgb(231, 248, 250)"}
|
||||
pressFunc={() => {
|
||||
props.navigation.navigate("calendar");
|
||||
setPageIndex(0);
|
||||
setToDosIndex(0);
|
||||
setUserView(true);
|
||||
setIsFamilyView(false);
|
||||
}}
|
||||
icon={<NavCalendarIcon />}
|
||||
/>
|
||||
<DrawerButton
|
||||
color="#50be0c"
|
||||
title={"Groceries"}
|
||||
bgColor={"#eef9e7"}
|
||||
pressFunc={() => {
|
||||
props.navigation.navigate("grocery");
|
||||
setPageIndex(0);
|
||||
setToDosIndex(0);
|
||||
setUserView(true);
|
||||
setIsFamilyView(false);
|
||||
}}
|
||||
icon={<NavGroceryIcon />}
|
||||
/>
|
||||
<DrawerButton
|
||||
color="#ea156d"
|
||||
title={"Feedback"}
|
||||
bgColor={"#fdedf4"}
|
||||
pressFunc={() => {
|
||||
props.navigation.navigate("feedback");
|
||||
setPageIndex(0);
|
||||
setToDosIndex(0);
|
||||
setUserView(true);
|
||||
setIsFamilyView(false);
|
||||
}}
|
||||
icon={<FeedbackNavIcon />}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
{/*<DrawerButton
|
||||
color="#fd1775"
|
||||
title={"My Reminders"}
|
||||
bgColor={"#ffe8f2"}
|
||||
pressFunc={() => props.navigation.navigate("reminders")}
|
||||
icon={
|
||||
<FontAwesome6
|
||||
name="clock-rotate-left"
|
||||
size={28}
|
||||
color="#fd1775"
|
||||
/>
|
||||
}
|
||||
/>*/}
|
||||
<DrawerButton
|
||||
color="#8005eb"
|
||||
title={"To Do's"}
|
||||
bgColor={"#f3e6fd"}
|
||||
pressFunc={() => {
|
||||
props.navigation.navigate("todos");
|
||||
setPageIndex(0);
|
||||
setToDosIndex(0);
|
||||
setUserView(true);
|
||||
setIsFamilyView(false);
|
||||
}}
|
||||
icon={<NavToDosIcon />}
|
||||
/>
|
||||
<DrawerButton
|
||||
color="#e0ca03"
|
||||
title={"Brain Dump"}
|
||||
bgColor={"#fffacb"}
|
||||
pressFunc={() => {
|
||||
props.navigation.navigate("brain_dump");
|
||||
setPageIndex(0);
|
||||
setToDosIndex(0);
|
||||
setUserView(true);
|
||||
setIsFamilyView(false);
|
||||
}}
|
||||
icon={<NavBrainDumpIcon />}
|
||||
/>
|
||||
{/*<DrawerItem label="Logout" onPress={() => signOut()} />*/}
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
onPress={() => {
|
||||
props.navigation.navigate("settings");
|
||||
setPageIndex(0);
|
||||
setToDosIndex(0);
|
||||
setUserView(true);
|
||||
setIsFamilyView(false);
|
||||
}}
|
||||
label={"Manage Settings"}
|
||||
labelStyle={styles.label}
|
||||
iconSource={() => (
|
||||
<View
|
||||
backgroundColor="#ededed"
|
||||
width={60}
|
||||
height={60}
|
||||
style={{ borderRadius: 50 }}
|
||||
marginR-10
|
||||
centerV
|
||||
centerH
|
||||
>
|
||||
<NavSettingsIcon />
|
||||
</View>
|
||||
)}
|
||||
backgroundColor="white"
|
||||
color="#464039"
|
||||
paddingV-30
|
||||
marginH-30
|
||||
marginB-10
|
||||
borderRadius={18.55}
|
||||
style={{ elevation: 0 }}
|
||||
/>
|
||||
type DrawerParamList = {
|
||||
index: undefined;
|
||||
calendar: undefined;
|
||||
brain_dump: undefined;
|
||||
settings: undefined;
|
||||
grocery: undefined;
|
||||
reminders: undefined;
|
||||
todos: undefined;
|
||||
notifications: undefined;
|
||||
feedback: undefined;
|
||||
};
|
||||
|
||||
<Button
|
||||
size={ButtonSize.large}
|
||||
marginH-30
|
||||
marginT-12
|
||||
paddingV-15
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
borderWidth: 1.3,
|
||||
borderColor: "#fd1775",
|
||||
}}
|
||||
label="Sign out of Cally"
|
||||
color="#fd1775"
|
||||
labelStyle={styles.signOut}
|
||||
onPress={() => signOut()}
|
||||
/>
|
||||
</DrawerContentScrollView>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Drawer.Screen
|
||||
name="index"
|
||||
options={{
|
||||
drawerLabel: "Calendar",
|
||||
title: "Calendar",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="calendar"
|
||||
options={{
|
||||
drawerLabel: "Calendar",
|
||||
title: "Calendar",
|
||||
drawerItemStyle: { display: "none" },
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="brain_dump"
|
||||
options={{
|
||||
drawerLabel: "Brain Dump",
|
||||
title: "Brain Dump",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="settings"
|
||||
options={{
|
||||
drawerLabel: "Settings",
|
||||
title: "Settings",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="grocery"
|
||||
options={{
|
||||
drawerLabel: "Grocery",
|
||||
title: "Grocery",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="reminders"
|
||||
options={{
|
||||
drawerLabel: "Reminders",
|
||||
title: "Reminders",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="todos"
|
||||
options={{
|
||||
drawerLabel: "To-Do",
|
||||
title: "To-Dos",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="feedback"
|
||||
options={{ drawerLabel: "Feedback", title: "Feedback" }}
|
||||
/>
|
||||
</Drawer>
|
||||
);
|
||||
type DrawerScreenNavigationProp = DrawerNavigationProp<DrawerParamList>;
|
||||
|
||||
interface DrawerButtonConfig {
|
||||
id: string;
|
||||
title: string;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
icon: React.FC;
|
||||
route: keyof DrawerParamList;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
signOut: { fontFamily: "Poppins_500Medium", fontSize: 15 },
|
||||
label: { fontFamily: "Poppins_400Medium", fontSize: 15 },
|
||||
title: {
|
||||
fontSize: 26.13,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
color: "#262627",
|
||||
},
|
||||
const DRAWER_BUTTONS: DrawerButtonConfig[] = [
|
||||
{
|
||||
id: 'calendar',
|
||||
title: 'Calendar',
|
||||
color: 'rgb(7, 184, 199)',
|
||||
bgColor: 'rgb(231, 248, 250)',
|
||||
icon: NavCalendarIcon,
|
||||
route: 'calendar'
|
||||
},
|
||||
{
|
||||
id: 'grocery',
|
||||
title: 'Groceries',
|
||||
color: '#50be0c',
|
||||
bgColor: '#eef9e7',
|
||||
icon: NavGroceryIcon,
|
||||
route: 'grocery'
|
||||
},
|
||||
{
|
||||
id: 'feedback',
|
||||
title: 'Feedback',
|
||||
color: '#ea156d',
|
||||
bgColor: '#fdedf4',
|
||||
icon: FeedbackNavIcon,
|
||||
route: 'feedback'
|
||||
},
|
||||
{
|
||||
id: 'todos',
|
||||
title: 'To Dos',
|
||||
color: '#8005eb',
|
||||
bgColor: '#f3e6fd',
|
||||
icon: NavToDosIcon,
|
||||
route: 'todos'
|
||||
},
|
||||
{
|
||||
id: 'brain_dump',
|
||||
title: 'Brain Dump',
|
||||
color: '#e0ca03',
|
||||
bgColor: '#fffacb',
|
||||
icon: NavBrainDumpIcon,
|
||||
route: 'brain_dump'
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
title: 'Notifications',
|
||||
color: '#ffa200',
|
||||
bgColor: '#ffdda1',
|
||||
icon: () => <Ionicons name="notifications-outline" size={24} color="#ffa200"/>,
|
||||
route: 'notifications'
|
||||
}
|
||||
];
|
||||
|
||||
interface DrawerContentProps {
|
||||
props: DrawerContentComponentProps;
|
||||
}
|
||||
|
||||
const DrawerContent: React.FC<DrawerContentProps> = ({props}) => {
|
||||
const {mutateAsync: signOut} = useSignOut();
|
||||
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
||||
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||
const setUserView = useSetAtom(userSettingsView);
|
||||
const setToDosIndex = useSetAtom(toDosPageIndex);
|
||||
|
||||
const handleNavigation = useCallback((route: keyof DrawerParamList) => {
|
||||
props.navigation.navigate(route);
|
||||
setPageIndex(0);
|
||||
setToDosIndex(0);
|
||||
setUserView(true);
|
||||
setIsFamilyView(false);
|
||||
}, [props.navigation, setPageIndex, setToDosIndex, setUserView, setIsFamilyView]);
|
||||
|
||||
const renderDrawerButtons = () => {
|
||||
const midPoint = Math.ceil(DRAWER_BUTTONS.length / 2);
|
||||
const leftButtons = DRAWER_BUTTONS.slice(0, midPoint);
|
||||
const rightButtons = DRAWER_BUTTONS.slice(midPoint);
|
||||
|
||||
return (
|
||||
<View row paddingH-30>
|
||||
<View flex-1 paddingR-5>
|
||||
{leftButtons.map(button => (
|
||||
<DrawerButton
|
||||
key={button.id}
|
||||
title={button.title}
|
||||
color={button.color}
|
||||
bgColor={button.bgColor}
|
||||
pressFunc={() => handleNavigation(button.route)}
|
||||
icon={<button.icon/>}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<View flex-1>
|
||||
{rightButtons.map(button => (
|
||||
<DrawerButton
|
||||
key={button.id}
|
||||
title={button.title}
|
||||
color={button.color}
|
||||
bgColor={button.bgColor}
|
||||
pressFunc={() => handleNavigation(button.route)}
|
||||
icon={<button.icon/>}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerContentScrollView {...props}>
|
||||
<View centerV marginH-30 marginT-20 marginB-20 row>
|
||||
<ImageBackground
|
||||
source={require("../../assets/images/splash.png")}
|
||||
style={styles.logo}
|
||||
/>
|
||||
<Text style={styles.title}>Welcome to Cally</Text>
|
||||
</View>
|
||||
|
||||
{renderDrawerButtons()}
|
||||
|
||||
<Button
|
||||
onPress={() => handleNavigation('settings')}
|
||||
label="Manage Settings"
|
||||
labelStyle={styles.label}
|
||||
iconSource={() => (
|
||||
<View style={styles.settingsIcon}>
|
||||
<NavSettingsIcon/>
|
||||
</View>
|
||||
)}
|
||||
backgroundColor="white"
|
||||
color="#464039"
|
||||
paddingV-30
|
||||
marginH-30
|
||||
borderRadius={18.55}
|
||||
style={{elevation: 0}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
size={ButtonSize.large}
|
||||
style={styles.signOutButton}
|
||||
label="Sign out of Cally"
|
||||
color="#fd1775"
|
||||
labelStyle={styles.signOut}
|
||||
onPress={() => signOut()}
|
||||
/>
|
||||
</DrawerContentScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
interface HeaderRightProps {
|
||||
route: RouteProp<DrawerParamList>;
|
||||
navigation: DrawerScreenNavigationProp;
|
||||
}
|
||||
|
||||
const HeaderRight: React.FC<HeaderRightProps> = memo(({route, navigation}) => {
|
||||
const showViewSwitch = ["calendar", "todos", "index"].includes(route.name);
|
||||
const isCalendarPage = ["calendar", "index"].includes(route.name);
|
||||
|
||||
if (!isTablet || !showViewSwitch) {
|
||||
return isCalendarPage ? <CalendarHeader/> : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View marginR-16 row centerV>
|
||||
{isTablet && isCalendarPage && (
|
||||
<View flex-1 center>
|
||||
<CalendarHeader/>
|
||||
</View>
|
||||
)}
|
||||
<ViewSwitch navigation={navigation}/>
|
||||
</View>
|
||||
)
|
||||
});
|
||||
|
||||
interface DrawerScreen {
|
||||
name: keyof DrawerParamList;
|
||||
title: string;
|
||||
hideInDrawer?: boolean;
|
||||
}
|
||||
|
||||
const DRAWER_SCREENS: DrawerScreen[] = [
|
||||
{name: 'index', title: 'Calendar'},
|
||||
{name: 'calendar', title: 'Calendar', hideInDrawer: true},
|
||||
{name: 'brain_dump', title: 'Brain Dump'},
|
||||
{name: 'settings', title: 'Settings'},
|
||||
{name: 'grocery', title: 'Groceries'},
|
||||
{name: 'reminders', title: 'Reminders'},
|
||||
{name: 'todos', title: isTablet ? 'Family To Dos' : 'To Dos'},
|
||||
{name: 'notifications', title: 'Notifications'},
|
||||
{name: 'feedback', title: 'Feedback'}
|
||||
];
|
||||
|
||||
const TabLayout: React.FC = () => {
|
||||
const screenOptions = useMemo(() => {
|
||||
return ({route, navigation}: {
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
navigation: DrawerNavigationProp<ParamListBase, string>;
|
||||
theme: Theme;
|
||||
}): DrawerNavigationOptions => ({
|
||||
lazy: true,
|
||||
headerShown: true,
|
||||
headerTitleAlign: "left",
|
||||
headerTitle: ({children}) => {
|
||||
const isCalendarRoute = ["calendar", "index"].includes(route.name);
|
||||
if (isCalendarRoute) return null;
|
||||
|
||||
return (
|
||||
<View flexG centerV paddingL-10>
|
||||
<Text style={styles.headerTitle}>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
headerLeft: () => (
|
||||
<Pressable
|
||||
onPress={() => navigation.toggleDrawer()}
|
||||
hitSlop={{top: 10, bottom: 10, left: 10, right: 10}}
|
||||
style={({pressed}) => [styles.drawerTrigger, {opacity: pressed ? 0.4 : 1}]}
|
||||
>
|
||||
<DrawerIcon/>
|
||||
</Pressable>
|
||||
),
|
||||
headerRight: () => <HeaderRight
|
||||
route={route as RouteProp<DrawerParamList>}
|
||||
navigation={navigation as DrawerNavigationProp<DrawerParamList>}
|
||||
/>,
|
||||
drawerStyle: styles.drawer,
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
initialRouteName="index"
|
||||
detachInactiveScreens
|
||||
screenOptions={screenOptions}
|
||||
drawerContent={(props) => <DrawerContent props={props}/>}
|
||||
>
|
||||
{DRAWER_SCREENS.map(screen => (
|
||||
<Drawer.Screen
|
||||
key={screen.name}
|
||||
name={screen.name}
|
||||
options={{
|
||||
drawerLabel: screen.title,
|
||||
title: screen.title,
|
||||
...(screen.hideInDrawer && {drawerItemStyle: {display: 'none'}}),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
drawer: {
|
||||
width: isTablet ? "30%" : "90%",
|
||||
backgroundColor: "#f9f8f7",
|
||||
height: "100%",
|
||||
},
|
||||
drawerTrigger: {
|
||||
marginLeft: 16,
|
||||
},
|
||||
headerTitle: {
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
fontSize: isTablet ? 22 : 17,
|
||||
},
|
||||
logo: {
|
||||
backgroundColor: "transparent",
|
||||
height: 51.43,
|
||||
aspectRatio: 1,
|
||||
marginRight: 8,
|
||||
},
|
||||
settingsIcon: {
|
||||
backgroundColor: "#ededed",
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 50,
|
||||
marginRight: 10,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
signOutButton: {
|
||||
marginTop: 50,
|
||||
marginHorizontal: 10,
|
||||
paddingVertical: 15,
|
||||
backgroundColor: "transparent",
|
||||
borderWidth: 1.3,
|
||||
borderColor: "#fd1775",
|
||||
},
|
||||
signOut: {
|
||||
fontFamily: "Poppins_500Medium",
|
||||
fontSize: 15,
|
||||
},
|
||||
label: {
|
||||
fontFamily: "Poppins_400Medium",
|
||||
fontSize: 15,
|
||||
},
|
||||
title: {
|
||||
fontSize: 26.13,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
color: "#262627",
|
||||
},
|
||||
});
|
||||
|
||||
export default TabLayout;
|
||||
@ -1,14 +1,13 @@
|
||||
import {BrainDumpProvider} from "@/contexts/DumpContext";
|
||||
import {View} from "react-native-ui-lib";
|
||||
import BrainDumpPage from "@/components/pages/brain_dump/BrainDumpPage";
|
||||
import { BrainDumpProvider } from "@/contexts/DumpContext";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { View } from "react-native-ui-lib";
|
||||
|
||||
export default function Screen() {
|
||||
return (
|
||||
<BrainDumpProvider>
|
||||
<View>
|
||||
<BrainDumpPage />
|
||||
</View>
|
||||
</BrainDumpProvider>
|
||||
);
|
||||
return (
|
||||
<BrainDumpProvider>
|
||||
<View>
|
||||
<BrainDumpPage/>
|
||||
</View>
|
||||
</BrainDumpProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,18 +1,92 @@
|
||||
import React from "react";
|
||||
import React, {useEffect} from "react";
|
||||
import {RefreshControl, ScrollView, View} from "react-native";
|
||||
import CalendarPage from "@/components/pages/calendar/CalendarPage";
|
||||
import { View } from "react-native-ui-lib";
|
||||
import TabletCalendarPage from "@/components/pages/(tablet_pages)/calendar/TabletCalendarPage";
|
||||
import { DeviceType } from "expo-device";
|
||||
import * as Device from "expo-device";
|
||||
import {DeviceType} from "expo-device";
|
||||
import {useCalSync} from "@/hooks/useCalSync";
|
||||
import {colorMap} from "@/constants/colorMap";
|
||||
import {useSetAtom} from "jotai";
|
||||
import {selectedUserAtom} from "@/components/pages/calendar/atoms";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
|
||||
export default function Screen() {
|
||||
return (
|
||||
<View style={{ backgroundColor: "white" }}>
|
||||
{Device.deviceType === DeviceType.TABLET ? (
|
||||
<TabletCalendarPage />
|
||||
) : (
|
||||
<CalendarPage />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const isTablet = Device.deviceType === DeviceType.TABLET;
|
||||
const {resyncAllCalendars, isSyncing} = useCalSync();
|
||||
const setSelectedUser = useSetAtom(selectedUserAtom);
|
||||
const {profileData} = useAuthContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTablet && profileData) setSelectedUser({
|
||||
uid: profileData.uid!,
|
||||
firstName: profileData.firstName,
|
||||
lastName: profileData.lastName,
|
||||
eventColor: profileData?.eventColor!
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
try {
|
||||
await resyncAllCalendars();
|
||||
} catch (error) {
|
||||
console.error("Refresh failed:", error);
|
||||
}
|
||||
}, [resyncAllCalendars]);
|
||||
|
||||
const refreshControl = (
|
||||
<RefreshControl
|
||||
colors={[
|
||||
colorMap.pink,
|
||||
colorMap.green,
|
||||
colorMap.orange,
|
||||
colorMap.purple,
|
||||
colorMap.teal,
|
||||
]}
|
||||
tintColor={colorMap.pink}
|
||||
progressBackgroundColor="white"
|
||||
refreshing={isSyncing}
|
||||
onRefresh={onRefresh}
|
||||
style={isTablet ? {
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
transform: [{translateX: -20}],
|
||||
} : undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isTablet) {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<View style={{flex: 1, zIndex: 0}}>
|
||||
<TabletCalendarPage/>
|
||||
</View>
|
||||
<ScrollView
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "15%",
|
||||
height: "9%",
|
||||
width: "62%",
|
||||
zIndex: 50,
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
paddingRight: 200,
|
||||
}}
|
||||
bounces={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
pointerEvents={isSyncing ? "auto" : "none"}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<CalendarPage/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
5
app/(auth)/notifications/_layout.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import {Stack} from "expo-router";
|
||||
|
||||
export default function StackLayout () {
|
||||
return <Stack screenOptions={{headerShown: false}}/>
|
||||
}
|
||||
7
app/(auth)/notifications/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import NotificationsPage from "@/components/pages/notifications/NotificationsPage";
|
||||
|
||||
export default function Screen() {
|
||||
return (
|
||||
<NotificationsPage/>
|
||||
);
|
||||
}
|
||||
@ -1,25 +1,16 @@
|
||||
import TabletChoresPage from "@/components/pages/(tablet_pages)/chores/TabletChoresPage";
|
||||
import AddChore from "@/components/pages/todos/AddChore";
|
||||
import ProgressCard from "@/components/pages/todos/ProgressCard";
|
||||
import ToDoItem from "@/components/pages/todos/ToDoItem";
|
||||
import ToDosList from "@/components/pages/todos/ToDosList";
|
||||
import ToDosPage from "@/components/pages/todos/ToDosPage";
|
||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { ToDosContextProvider, useToDosContext } from "@/contexts/ToDosContext";
|
||||
import { AntDesign } from "@expo/vector-icons";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { Button, ButtonSize, View, Text, Constants } from "react-native-ui-lib";
|
||||
import {ToDosContextProvider} from "@/contexts/ToDosContext";
|
||||
import * as Device from "expo-device";
|
||||
|
||||
export default function Screen() {
|
||||
return (
|
||||
<ToDosContextProvider>
|
||||
{Device.deviceType === Device.DeviceType.TABLET ? (
|
||||
<TabletChoresPage />
|
||||
) : (
|
||||
<ToDosPage />
|
||||
)}
|
||||
</ToDosContextProvider>
|
||||
);
|
||||
return (
|
||||
<ToDosContextProvider>
|
||||
{Device.deviceType === Device.DeviceType.TABLET ? (
|
||||
<TabletChoresPage/>
|
||||
) : (
|
||||
<ToDosPage/>
|
||||
)}
|
||||
</ToDosContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
117
app/(unauth)/birthday_page.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { Button, Text, View, DateTimePicker } from "react-native-ui-lib";
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "expo-router";
|
||||
import { Platform, StyleSheet } from "react-native";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
|
||||
|
||||
export default function BirthdayScreen() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuthContext();
|
||||
const [date, setDate] = useState(new Date());
|
||||
const { mutateAsync: updateUserData } = useUpdateUserData();
|
||||
|
||||
const onDateChange = (event: any, selectedDate?: Date) => {
|
||||
const currentDate = selectedDate || date;
|
||||
setDate(currentDate);
|
||||
};
|
||||
|
||||
const handleContinue = async () => {
|
||||
try {
|
||||
updateUserData({
|
||||
newUserData: {
|
||||
birthday: date,
|
||||
},
|
||||
}).then(() => router.push("/(unauth)/cal_sync"));
|
||||
} catch (error) {
|
||||
console.error("Error saving birthday:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getMaxDate = () => {
|
||||
const date = new Date();
|
||||
date.setFullYear(date.getFullYear() - 3); // Minimum age: 3 years
|
||||
return date;
|
||||
};
|
||||
|
||||
const getMinDate = () => {
|
||||
const date = new Date();
|
||||
date.setFullYear(date.getFullYear() - 18); // Maximum age: 18 years
|
||||
return date;
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 21,
|
||||
paddingBottom: 45,
|
||||
paddingTop: "20%",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<View gap-13 width={"100%"} marginB-20>
|
||||
<Text style={{ fontSize: 40, fontFamily: "Manrope_600SemiBold" }}>
|
||||
When's your birthday?
|
||||
</Text>
|
||||
<Text color={"#919191"} style={{ fontSize: 20 }}>
|
||||
We'll use this to celebrate your special day!
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View width={"100%"} flexG>
|
||||
<DateTimePicker
|
||||
value={date}
|
||||
mode="date"
|
||||
minimumDate={getMinDate()}
|
||||
maximumDate={getMaxDate()}
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
const validDate = new Date(date);
|
||||
if (!isNaN(validDate.getTime())) {
|
||||
setDate(validDate);
|
||||
}
|
||||
}
|
||||
}}
|
||||
style={styles.textfield}
|
||||
textAlign="center"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View flexG />
|
||||
|
||||
<View width={"100%"}>
|
||||
<Button
|
||||
label="Continue"
|
||||
onPress={handleContinue}
|
||||
style={{
|
||||
height: 50,
|
||||
}}
|
||||
backgroundColor="#fd1775"
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||
fontSize: 16,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
textfield: {
|
||||
backgroundColor: "white",
|
||||
marginVertical: 100,
|
||||
padding: 30,
|
||||
height: 44,
|
||||
borderRadius: 50,
|
||||
fontFamily: "PlusJakartaSans_300Light",
|
||||
fontSize: 15,
|
||||
color: "#919191",
|
||||
alignContent: "center",
|
||||
},
|
||||
});
|
||||
@ -1,17 +1,24 @@
|
||||
import {SafeAreaView} from "react-native-safe-area-context";
|
||||
import {Button, Text, View} from "react-native-ui-lib";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import {useCalSync} from "@/hooks/useCalSync";
|
||||
import GoogleIcon from "@/assets/svgs/GoogleIcon";
|
||||
import AppleIcon from "@/assets/svgs/AppleIcon";
|
||||
import OutlookIcon from "@/assets/svgs/OutlookIcon";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {StyleSheet} from "react-native";
|
||||
import { useGetHouseholdName } from "@/hooks/firebase/useGetHouseholdName";
|
||||
|
||||
export default function Screen() {
|
||||
const {profileData, setRedirectOverride} = useAuthContext()
|
||||
const {handleStartGoogleSignIn, handleAppleSignIn, handleMicrosoftSignIn} = useCalSync()
|
||||
const {data: householdName, refetch} = useGetHouseholdName(profileData?.familyId);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [profileData?.familyId])
|
||||
|
||||
const hasSomeCalendarsSynced =
|
||||
!!profileData?.appleAccounts || !!profileData?.microsoftAccounts || !!profileData?.googleAccounts
|
||||
|
||||
@ -19,6 +26,9 @@ export default function Screen() {
|
||||
<SafeAreaView style={{flex: 1}}>
|
||||
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
|
||||
<View gap-13 width={"100%"} marginB-20>
|
||||
{householdName && <Text style={{fontSize: 25, fontFamily: 'Manrope_600SemiBold'}}>
|
||||
You Joined The {householdName} Household
|
||||
</Text>}
|
||||
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}>
|
||||
Let's get started!
|
||||
</Text>
|
||||
|
||||
@ -1,169 +1,229 @@
|
||||
import {SafeAreaView} from "react-native-safe-area-context";
|
||||
import {Button, Colors, Dialog, LoaderScreen, Text, View} from "react-native-ui-lib";
|
||||
import React, {useCallback, useState} from "react";
|
||||
import {useRouter} from "expo-router";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import {
|
||||
Button,
|
||||
Colors,
|
||||
Dialog,
|
||||
LoaderScreen,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native-ui-lib";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter } from "expo-router";
|
||||
import QRIcon from "@/assets/svgs/QRIcon";
|
||||
import {Camera, CameraView} from "expo-camera";
|
||||
import {useLoginWithQrCode} from "@/hooks/firebase/useLoginWithQrCode";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import { Camera, CameraView } from "expo-camera";
|
||||
import { useLoginWithQrCode } from "@/hooks/firebase/useLoginWithQrCode";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import debounce from "debounce";
|
||||
import * as Device from "expo-device";
|
||||
import { DeviceType } from "expo-device";
|
||||
import { Dimensions } from "react-native";
|
||||
|
||||
export default function Screen() {
|
||||
const router = useRouter()
|
||||
const {setRedirectOverride} = useAuthContext()
|
||||
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
|
||||
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
const { setRedirectOverride } = useAuthContext();
|
||||
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
|
||||
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
|
||||
|
||||
const {mutateAsync: signInWithQrCode, isLoading} = useLoginWithQrCode();
|
||||
const isTablet: boolean = Device.deviceType === DeviceType.TABLET;
|
||||
const [isPortrait, setIsPortrait] = useState(() => {
|
||||
const dim = Dimensions.get('screen');
|
||||
return dim.height >= dim.width;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
|
||||
setIsPortrait(screen.height >= screen.width);
|
||||
});
|
||||
|
||||
return () => subscription.remove();
|
||||
}, []);
|
||||
|
||||
const debouncedRouterReplace = useCallback(
|
||||
debounce(() => {
|
||||
router.push("/(unauth)/cal_sync");
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleQrCodeScanned = async ({data}: { data: string }) => {
|
||||
setShowCameraDialog(false);
|
||||
setRedirectOverride(true);
|
||||
try {
|
||||
await signInWithQrCode({userId: data});
|
||||
debouncedRouterReplace()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
};
|
||||
|
||||
const getCameraPermissions = async (callback: () => void) => {
|
||||
const {status} = await Camera.requestCameraPermissionsAsync();
|
||||
setHasPermission(status === "granted");
|
||||
if (status === "granted") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenQrCodeDialog = () => {
|
||||
getCameraPermissions(() => setShowCameraDialog(true));
|
||||
const getTopPadding = () => {
|
||||
if (Device.deviceType === DeviceType.TABLET) {
|
||||
return isPortrait ? "50%" : "15%";
|
||||
}
|
||||
return "20%"; // non-tablet case, regardless of orientation
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{flex: 1}}>
|
||||
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
|
||||
<View center>
|
||||
<Text style={{fontSize: 30, fontFamily: 'Manrope_600SemiBold'}}>
|
||||
Get started with Cally
|
||||
</Text>
|
||||
</View>
|
||||
const { mutateAsync: signInWithQrCode, isLoading } = useLoginWithQrCode();
|
||||
|
||||
<View width={"100%"} gap-30>
|
||||
<View>
|
||||
<Button
|
||||
label="Scan QR Code"
|
||||
marginT-50
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_400Regular",
|
||||
fontSize: 16,
|
||||
marginLeft: 10
|
||||
}}
|
||||
iconSource={() => <QRIcon color={"#07B8C7"}/>}
|
||||
onPress={handleOpenQrCodeDialog}
|
||||
style={{height: 50}}
|
||||
color={Colors.black}
|
||||
backgroundColor={Colors.white}
|
||||
/>
|
||||
{/* GOOGLE LOGIN HERE */}
|
||||
</View>
|
||||
const debouncedRouterReplace = useCallback(
|
||||
debounce(() => {
|
||||
router.push("/(unauth)/birthday_page");
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
<View row center gap-20>
|
||||
<View flexG style={{backgroundColor: "#E2E2E2", height: 2}}/>
|
||||
<Text style={{fontSize: 16, fontFamily: 'PlusJakartaSans_300Light', color: "#7A7A7A"}}>
|
||||
or
|
||||
</Text>
|
||||
<View flexG style={{backgroundColor: "#E2E2E2", height: 2}}/>
|
||||
</View>
|
||||
const handleQrCodeScanned = async ({ data }: { data: string }) => {
|
||||
setShowCameraDialog(false);
|
||||
setRedirectOverride(true);
|
||||
try {
|
||||
await signInWithQrCode({ userId: data });
|
||||
debouncedRouterReplace();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
<View>
|
||||
<Button
|
||||
label="Contine with Email"
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_400Regular",
|
||||
fontSize: 16,
|
||||
marginLeft: 10
|
||||
}}
|
||||
onPress={() => router.push("/(unauth)/sign_up")}
|
||||
style={{height: 50, borderStyle: "solid", borderColor: "#E2E2E2", borderWidth: 2}}
|
||||
color={Colors.black}
|
||||
backgroundColor={"transparent"}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
const getCameraPermissions = async (callback: () => void) => {
|
||||
const { status } = await Camera.requestCameraPermissionsAsync();
|
||||
setHasPermission(status === "granted");
|
||||
if (status === "granted") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenQrCodeDialog = () => {
|
||||
getCameraPermissions(() => setShowCameraDialog(true));
|
||||
};
|
||||
|
||||
<View flexG/>
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, alignItems: "center" }}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 21,
|
||||
paddingBottom: 45,
|
||||
paddingTop: isLoading ? "20%" : getTopPadding(),
|
||||
width: isTablet ? 629 : '100%'
|
||||
}}
|
||||
>
|
||||
<View center>
|
||||
<Text style={{ fontSize: 30, fontFamily: "Manrope_600SemiBold" }}>
|
||||
Get started with Cally
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View row centerH gap-5>
|
||||
<Text style={{
|
||||
fontFamily: "PlusJakartaSans_300Light",
|
||||
fontSize: 16,
|
||||
color: "#484848"
|
||||
}} center>
|
||||
Already have an account?
|
||||
</Text>
|
||||
<View width={"100%"} gap-30>
|
||||
<View>
|
||||
<Button
|
||||
label="Scan QR Code"
|
||||
marginT-50
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_400Regular",
|
||||
fontSize: 16,
|
||||
marginLeft: 10,
|
||||
}}
|
||||
iconSource={() => <QRIcon color={"#07B8C7"} />}
|
||||
onPress={handleOpenQrCodeDialog}
|
||||
style={{ height: 50 }}
|
||||
color={Colors.black}
|
||||
backgroundColor={Colors.white}
|
||||
/>
|
||||
{/* GOOGLE LOGIN HERE */}
|
||||
</View>
|
||||
|
||||
<Button
|
||||
label="Log in"
|
||||
link
|
||||
onPress={() => router.push("/(unauth)/sign_in")}
|
||||
labelStyle={[
|
||||
{
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
color: "#919191",
|
||||
},
|
||||
{fontSize: 16, textDecorationLine: "none", color: "#fd1775"},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Legacy, move into separate component */}
|
||||
{/* Camera Dialog */}
|
||||
<Dialog
|
||||
visible={showCameraDialog}
|
||||
onDismiss={() => setShowCameraDialog(false)}
|
||||
bottom
|
||||
width="100%"
|
||||
height="70%"
|
||||
containerStyle={{padding: 15, backgroundColor: "white"}}
|
||||
<View row center gap-20>
|
||||
<View flexG style={{ backgroundColor: "#E2E2E2", height: 2 }} />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontFamily: "PlusJakartaSans_300Light",
|
||||
color: "#7A7A7A",
|
||||
}}
|
||||
>
|
||||
<Text center style={{fontSize: 16}} marginB-15>
|
||||
Scan a QR code presented from your family member of provider.
|
||||
</Text>
|
||||
{hasPermission === null ? (
|
||||
<Text>Requesting camera permissions...</Text>
|
||||
) : !hasPermission ? (
|
||||
<Text>No access to camera</Text>
|
||||
) : (
|
||||
<CameraView
|
||||
style={{flex: 1, borderRadius: 15}}
|
||||
onBarcodeScanned={handleQrCodeScanned}
|
||||
barcodeScannerSettings={{
|
||||
barcodeTypes: ["qr"],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
label="Cancel"
|
||||
onPress={() => setShowCameraDialog(false)}
|
||||
backgroundColor="#fd1775"
|
||||
style={{margin: 10, marginBottom: 30}}
|
||||
/>
|
||||
</Dialog>
|
||||
or
|
||||
</Text>
|
||||
<View flexG style={{ backgroundColor: "#E2E2E2", height: 2 }} />
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Button
|
||||
label="Continue with Email"
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_400Regular",
|
||||
fontSize: 16,
|
||||
marginLeft: 10,
|
||||
}}
|
||||
onPress={() => router.push("/(unauth)/sign_up")}
|
||||
style={{
|
||||
height: 50,
|
||||
borderStyle: "solid",
|
||||
borderColor: "#E2E2E2",
|
||||
borderWidth: 2,
|
||||
}}
|
||||
color={Colors.black}
|
||||
backgroundColor={"transparent"}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{isLoading && (
|
||||
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white} color={Colors.grey40}/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
)
|
||||
{isTablet ? (
|
||||
<View marginT-30 />
|
||||
) : (
|
||||
<View flexG />
|
||||
)}
|
||||
|
||||
<View row centerH gap-5>
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: "PlusJakartaSans_300Light",
|
||||
fontSize: 16,
|
||||
color: "#484848",
|
||||
}}
|
||||
center
|
||||
>
|
||||
Already have an account?
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
label="Log in"
|
||||
link
|
||||
onPress={() => router.push("/(unauth)/sign_in")}
|
||||
labelStyle={[
|
||||
{
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
color: "#919191",
|
||||
},
|
||||
{ fontSize: 16, textDecorationLine: "none", color: "#fd1775" },
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Legacy, move into separate component */}
|
||||
{/* Camera Dialog */}
|
||||
<Dialog
|
||||
visible={showCameraDialog}
|
||||
onDismiss={() => setShowCameraDialog(false)}
|
||||
bottom
|
||||
width="100%"
|
||||
height="70%"
|
||||
containerStyle={{ padding: 15, backgroundColor: "white" }}
|
||||
>
|
||||
<Text center style={{ fontSize: 16 }} marginB-15>
|
||||
Scan a QR code presented from your family member of provider.
|
||||
</Text>
|
||||
{hasPermission === null ? (
|
||||
<Text>Requesting camera permissions...</Text>
|
||||
) : !hasPermission ? (
|
||||
<Text>No access to camera</Text>
|
||||
) : (
|
||||
<CameraView
|
||||
style={{ flex: 1, borderRadius: 15 }}
|
||||
onBarcodeScanned={handleQrCodeScanned}
|
||||
barcodeScannerSettings={{
|
||||
barcodeTypes: ["qr"],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
label="Cancel"
|
||||
onPress={() => setShowCameraDialog(false)}
|
||||
backgroundColor="#fd1775"
|
||||
style={{ margin: 10, marginBottom: 30 }}
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
{isLoading && (
|
||||
<LoaderScreen
|
||||
overlay
|
||||
message={"Signing in..."}
|
||||
backgroundColor={Colors.white}
|
||||
color={Colors.grey40}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
87
app/(unauth)/household_page.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { Button, Text, View, TextField } from "react-native-ui-lib";
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "expo-router";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { useUpdateHouseholdName } from "@/hooks/firebase/useUpdateHouseholdName";
|
||||
|
||||
export default function NewHouseholdScreen() {
|
||||
const router = useRouter();
|
||||
const { user, profileData } = useAuthContext();
|
||||
const [householdName, setHouseholdName] = useState("");
|
||||
const { mutateAsync: newHousehold } = useUpdateHouseholdName();
|
||||
|
||||
const handleContinue = async () => {
|
||||
try {
|
||||
if(profileData?.familyId)
|
||||
newHousehold({familyId: profileData?.familyId, name: householdName}).then(() => router.push("/(unauth)/cal_sync"));
|
||||
} catch (error) {
|
||||
console.error("Error saving household name:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 21,
|
||||
paddingBottom: 45,
|
||||
paddingTop: "20%",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<View gap-13 width={"100%"} marginB-20>
|
||||
<Text style={{ fontSize: 40, fontFamily: "Manrope_600SemiBold" }}>
|
||||
Name your household
|
||||
</Text>
|
||||
<Text color={"#919191"} style={{ fontSize: 20 }}>
|
||||
Give your family group a unique name!
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View width={"100%"} flexG>
|
||||
<TextField
|
||||
value={householdName}
|
||||
onChangeText={setHouseholdName}
|
||||
placeholder="Enter household name"
|
||||
style={styles.textfield}
|
||||
textAlign="center"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View flexG />
|
||||
|
||||
<View width={"100%"}>
|
||||
<Button
|
||||
label="Continue"
|
||||
onPress={handleContinue}
|
||||
style={{
|
||||
height: 50,
|
||||
}}
|
||||
backgroundColor="#fd1775"
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||
fontSize: 16,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
textfield: {
|
||||
backgroundColor: "white",
|
||||
marginVertical: 100,
|
||||
padding: 30,
|
||||
height: 44,
|
||||
borderRadius: 50,
|
||||
fontFamily: "PlusJakartaSans_300Light",
|
||||
fontSize: 15,
|
||||
color: "#919191",
|
||||
alignContent: "center",
|
||||
},
|
||||
});
|
||||
@ -2,6 +2,8 @@ import {SafeAreaView} from "react-native-safe-area-context";
|
||||
import {Button, Image, Text, View} from "react-native-ui-lib";
|
||||
import React from "react";
|
||||
import {useRouter} from "expo-router";
|
||||
import * as Device from "expo-device";
|
||||
import { DeviceType } from "expo-device";
|
||||
|
||||
export default function Screen() {
|
||||
const router = useRouter()
|
||||
@ -25,7 +27,7 @@ export default function Screen() {
|
||||
|
||||
<View flexG/>
|
||||
|
||||
<View width={"100%"}>
|
||||
<View width={"100%"} centerH>
|
||||
<Button
|
||||
label="Continue"
|
||||
marginT-50
|
||||
@ -34,7 +36,7 @@ export default function Screen() {
|
||||
fontSize: 16,
|
||||
}}
|
||||
onPress={() => router.push("/(unauth)/get_started")}
|
||||
style={{height: 50}}
|
||||
style={{height: 50, width: Device.deviceType === DeviceType.TABLET ? 629 : "100%"}}
|
||||
backgroundColor="#fd1775"
|
||||
/>
|
||||
</View>
|
||||
|
||||
@ -50,28 +50,29 @@ import {Stack} from "expo-router";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
import "react-native-reanimated";
|
||||
import {AuthContextProvider} from "@/contexts/AuthContext";
|
||||
import {QueryClient, QueryClientProvider} from "react-query";
|
||||
import {TextProps, ThemeManager, Toast, Typography,} from "react-native-ui-lib";
|
||||
import {Platform} from 'react-native';
|
||||
import KeyboardManager from 'react-native-keyboard-manager';
|
||||
import {enableScreens} from 'react-native-screens';
|
||||
import {enableScreens, enableFreeze} from 'react-native-screens';
|
||||
import {PersistQueryClientProvider} from "@/contexts/PersistQueryClientProvider";
|
||||
import auth from "@react-native-firebase/auth";
|
||||
import firestore from '@react-native-firebase/firestore';
|
||||
import functions from '@react-native-firebase/functions';
|
||||
|
||||
enableScreens(true)
|
||||
enableFreeze(true)
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
KeyboardManager.setEnable(true);
|
||||
KeyboardManager.setToolbarPreviousNextButtonEnable(true);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
// functions().useEmulator("localhost", 5001);
|
||||
// firestore().useEmulator("localhost", 5471);
|
||||
// auth().useEmulator("http://localhost:9099");
|
||||
// functions().useEmulator("localhost", 5001);
|
||||
// firestore().useEmulator("localhost", 5471);
|
||||
// auth().useEmulator("http://localhost:9099");
|
||||
}
|
||||
|
||||
type TextStyleBase =
|
||||
@ -211,8 +212,6 @@ export default function RootLayout() {
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded) {
|
||||
SplashScreen.hideAsync();
|
||||
|
||||
const typographies: Partial<Record<TextStyle, FontStyle>> = {};
|
||||
(
|
||||
[
|
||||
@ -262,7 +261,7 @@ export default function RootLayout() {
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PersistQueryClientProvider>
|
||||
<AuthContextProvider>
|
||||
<ThemeProvider value={DefaultTheme}>
|
||||
<Stack>
|
||||
@ -273,6 +272,6 @@ export default function RootLayout() {
|
||||
<Toast/>
|
||||
</ThemeProvider>
|
||||
</AuthContextProvider>
|
||||
</QueryClientProvider>
|
||||
</PersistQueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
assets/animations/todoCompletedAnimation.gif
Normal file
|
After Width: | Height: | Size: 19 KiB |
20
assets/svgs/CheckmarkIcon.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import * as React from "react"
|
||||
import Svg, { SvgProps, Path } from "react-native-svg"
|
||||
const CheckmarkIcon = (props: SvgProps) => (
|
||||
<Svg
|
||||
width={13}
|
||||
height={10}
|
||||
viewBox="0 0 13 10"
|
||||
fill={props.color || "white"}
|
||||
{...props}
|
||||
>
|
||||
<Path
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.95}
|
||||
d="m1.48 5.489 3.2 3.178 7.2-7.15"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
export default CheckmarkIcon
|
||||
19
assets/svgs/DrawerIcon.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
import Svg, { SvgProps, Path } from "react-native-svg"
|
||||
const DrawerIcon = (props: SvgProps) => (
|
||||
<Svg
|
||||
width={27}
|
||||
height={18}
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<Path
|
||||
stroke="#83807F"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2.7}
|
||||
d="M2 1.995h22.667M2 9.14h14.167M2 16.285h7.083"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
export default DrawerIcon
|
||||
17
assets/svgs/MenuDotsIcon.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import * as React from "react";
|
||||
import Svg, { SvgProps, Path } from "react-native-svg";
|
||||
const MenuDotsIcon = (props: SvgProps) => (
|
||||
<Svg
|
||||
width={props.width || 4}
|
||||
height={props.height || 15}
|
||||
viewBox="0 0 4 15"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<Path
|
||||
fill={props.color || "#7C7C7C"}
|
||||
d="M.88 7.563a1.56 1.56 0 1 0 3.12 0 1.56 1.56 0 0 0-3.12 0Zm0-5.2A1.56 1.56 0 1 0 4 2.426a1.56 1.56 0 0 0-3.12-.063Zm0 10.4A1.56 1.56 0 1 0 4 12.701a1.56 1.56 0 0 0-3.12.062Z"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
export default MenuDotsIcon;
|
||||
20
assets/svgs/TodoRepeatIcon.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import * as React from "react"
|
||||
import Svg, { SvgProps, Path } from "react-native-svg"
|
||||
const TodoRepeatIcon = (props: SvgProps) => (
|
||||
<Svg
|
||||
width={props.width || 30}
|
||||
height={props.height || 30}
|
||||
viewBox="-1 -3 19 19"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<Path
|
||||
stroke={props.color || "#858585"}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.455}
|
||||
d="M1.158 7.197a5.42 5.42 0 0 1 9.58-4.103m0 0V1.099m0 1.995v.037H8.705m3.21 2.71a5.42 5.42 0 0 1-9.444 4.263m0 .001v-.198h2.033m-2.033.198v1.835"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
export default TodoRepeatIcon
|
||||
@ -1,8 +1,17 @@
|
||||
module.exports = function (api) {
|
||||
const env = process.env.NODE_ENV;
|
||||
api.cache(true);
|
||||
|
||||
let plugins = [];
|
||||
|
||||
if (env !== 'development') {
|
||||
plugins.push('transform-remove-console');
|
||||
}
|
||||
|
||||
return {
|
||||
presets: [
|
||||
'babel-preset-expo'
|
||||
]
|
||||
'babel-preset-expo',
|
||||
],
|
||||
plugins
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -1,55 +1,64 @@
|
||||
export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) {
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/calendar/v3/calendars/primary/events?single_events=true&time_min=${startDate}&time_max=${endDate}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
const googleEvents = [];
|
||||
let pageToken = null;
|
||||
|
||||
do {
|
||||
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
|
||||
url.searchParams.set('singleEvents', 'true');
|
||||
url.searchParams.set('timeMin', startDate);
|
||||
url.searchParams.set('timeMax', endDate);
|
||||
if (pageToken) url.searchParams.set('pageToken', pageToken);
|
||||
|
||||
const data = await response.json();
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const googleEvents = [];
|
||||
data.items?.forEach((item) => {
|
||||
let isAllDay = false;
|
||||
const start = item.start;
|
||||
let startDateTime;
|
||||
if (start !== undefined) {
|
||||
if (start.dateTime) {
|
||||
const stringDate = start.dateTime;
|
||||
startDateTime = new Date(stringDate);
|
||||
} else {
|
||||
const stringDate = start.date;
|
||||
startDateTime = new Date(stringDate);
|
||||
isAllDay = true;
|
||||
}
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
const end = item.end;
|
||||
let endDateTime;
|
||||
if (end !== undefined) {
|
||||
if (end.dateTime) {
|
||||
const stringDate = end.dateTime;
|
||||
endDateTime = new Date(stringDate);
|
||||
} else {
|
||||
const stringDate = end.date;
|
||||
endDateTime = new Date(stringDate);
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
|
||||
}
|
||||
|
||||
const googleEvent = {
|
||||
id: item.id,
|
||||
title: item.summary ?? "",
|
||||
startDate: startDateTime,
|
||||
endDate: endDateTime,
|
||||
allDay: isAllDay,
|
||||
familyId,
|
||||
email
|
||||
};
|
||||
googleEvents.push(googleEvent);
|
||||
});
|
||||
data.items?.forEach((item) => {
|
||||
let isAllDay = false;
|
||||
let startDateTime, endDateTime;
|
||||
|
||||
return {googleEvents, success: response.ok};
|
||||
}
|
||||
if (item.start) {
|
||||
if (item.start.dateTime) {
|
||||
startDateTime = new Date(item.start.dateTime);
|
||||
} else if (item.start.date) {
|
||||
startDateTime = new Date(item.start.date);
|
||||
isAllDay = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.end) {
|
||||
if (item.end.dateTime) {
|
||||
endDateTime = new Date(item.end.dateTime);
|
||||
} else if (item.end.date) {
|
||||
endDateTime = new Date(item.end.date);
|
||||
isAllDay = true;
|
||||
}
|
||||
}
|
||||
|
||||
const googleEvent = {
|
||||
id: item.id,
|
||||
title: item.summary || "",
|
||||
startDate: startDateTime,
|
||||
endDate: endDateTime,
|
||||
allDay: isAllDay,
|
||||
familyId,
|
||||
email,
|
||||
};
|
||||
|
||||
googleEvents.push(googleEvent);
|
||||
});
|
||||
|
||||
// Prepare for the next page if it exists
|
||||
pageToken = data.nextPageToken;
|
||||
} while (pageToken);
|
||||
|
||||
return { googleEvents, success: true };
|
||||
}
|
||||
@ -1,93 +1,84 @@
|
||||
import { Text, TouchableOpacity, View } from "react-native-ui-lib";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { NavigationProp } from "@react-navigation/native";
|
||||
import view from "react-native-ui-lib/src/components/view";
|
||||
import {SegmentedControl, View} from "react-native-ui-lib";
|
||||
import React, {memo, useCallback, useMemo, useRef, useEffect} from "react";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {NavigationProp, useNavigationState} from "@react-navigation/native";
|
||||
|
||||
interface ViewSwitchProps {
|
||||
navigation: NavigationProp<any>; // Adjust according to your navigation structure
|
||||
navigation: NavigationProp<any>;
|
||||
}
|
||||
|
||||
const ViewSwitch: React.FC<ViewSwitchProps> = ({ navigation }) => {
|
||||
const [pageIndex, setPageIndex] = useState<number>(navigation.getState().index);
|
||||
const ViewSwitch = memo(function ViewSwitch({navigation}: ViewSwitchProps) {
|
||||
const currentIndex = useNavigationState((state) => state.index === 6 ? 1 : 0);
|
||||
const isInitialMount = useRef(true);
|
||||
const navigationPending = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
setPageIndex(navigation.getState().index);
|
||||
}, [navigation.getState().index])
|
||||
|
||||
return (
|
||||
<View
|
||||
row
|
||||
spread
|
||||
style={{
|
||||
borderRadius: 30,
|
||||
backgroundColor: "#ebebeb",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
// iOS shadow
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0,
|
||||
shadowRadius: 0,
|
||||
// Android shadow (elevation)
|
||||
elevation: 0,
|
||||
}}
|
||||
centerV
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
navigation.navigate("calendar");
|
||||
}}
|
||||
>
|
||||
<View
|
||||
centerV
|
||||
centerH
|
||||
height={54}
|
||||
paddingH-15
|
||||
style={ pageIndex == 1 || pageIndex == 0 ? styles.switchBtnActive : styles.switchBtn}
|
||||
>
|
||||
<Text color={pageIndex == 1 || pageIndex == 0 ? "white" : "black"} style={styles.switchTxt}>
|
||||
Calendar
|
||||
</Text>
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSegmentChange = useCallback(
|
||||
(index: number) => {
|
||||
if (navigationPending.current) return;
|
||||
|
||||
navigationPending.current = true;
|
||||
setTimeout(() => {
|
||||
navigation.navigate(index === 0 ? "calendar" : "todos");
|
||||
navigationPending.current = false;
|
||||
}, 300);
|
||||
},
|
||||
[navigation]
|
||||
);
|
||||
|
||||
const segments = useMemo(() => [
|
||||
{label: "Calendar", segmentLabelStyle: styles.labelStyle},
|
||||
{label: "To Dos", segmentLabelStyle: styles.labelStyle},
|
||||
], []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SegmentedControl
|
||||
segments={segments}
|
||||
containerStyle={styles.segmentContainer}
|
||||
style={styles.segment}
|
||||
backgroundColor="#ebebeb"
|
||||
inactiveColor="black"
|
||||
activeColor="white"
|
||||
activeBackgroundColor="#ea156c"
|
||||
outlineColor="white"
|
||||
outlineWidth={3}
|
||||
onChangeIndex={handleSegmentChange}
|
||||
initialIndex={currentIndex}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
navigation.navigate("todos");
|
||||
}}
|
||||
>
|
||||
<View
|
||||
centerV
|
||||
centerH
|
||||
height={54}
|
||||
paddingH-15
|
||||
style={pageIndex == 6 ? styles.switchBtnActive : styles.switchBtn}
|
||||
>
|
||||
<Text color={pageIndex == 6 ? "white" : "black"} style={styles.switchTxt}>
|
||||
Chores
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewSwitch;
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
switchBtnActive: {
|
||||
backgroundColor: "#ea156c",
|
||||
borderRadius: 50,
|
||||
width: 110,
|
||||
},
|
||||
switchBtn: {
|
||||
backgroundColor: "#ebebeb",
|
||||
borderRadius: 50,
|
||||
width: 110,
|
||||
},
|
||||
switchTxt: {
|
||||
fontSize: 16,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
container: {
|
||||
borderRadius: 30,
|
||||
backgroundColor: "#ebebeb",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {width: 0, height: 0},
|
||||
shadowOpacity: 0,
|
||||
shadowRadius: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
segmentContainer: {
|
||||
height: 44,
|
||||
width: 220,
|
||||
},
|
||||
segment: {
|
||||
borderRadius: 50,
|
||||
borderWidth: 0,
|
||||
height: 44,
|
||||
},
|
||||
labelStyle: {
|
||||
fontSize: 16,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
});
|
||||
|
||||
export default ViewSwitch;
|
||||
|
||||
@ -21,7 +21,9 @@ const TabletCalendarPage = () => {
|
||||
|
||||
return (
|
||||
<TabletContainer>
|
||||
<InnerCalendar />
|
||||
<View flexG paddingB-25>
|
||||
<InnerCalendar />
|
||||
</View>
|
||||
</TabletContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { View, Text, TouchableOpacity, Icon } from "react-native-ui-lib";
|
||||
import React, { useState } from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import { useToDosContext } from "@/contexts/ToDosContext";
|
||||
import {
|
||||
addDays,
|
||||
@ -12,9 +12,15 @@ import { AntDesign } from "@expo/vector-icons";
|
||||
import { IToDo } from "@/hooks/firebase/types/todoData";
|
||||
import ToDoItem from "../../todos/ToDoItem";
|
||||
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
|
||||
const groupToDosByDate = (toDos: IToDo[]) => {
|
||||
let sortedTodos = toDos.sort((a, b) => a.date - b.date);
|
||||
let sortedTodos = toDos.sort((a, b) => {
|
||||
const dateA = a.date === null ? new Date() : a.date;
|
||||
const dateB = b.date === null ? new Date() : b.date;
|
||||
return dateA - dateB;
|
||||
});
|
||||
|
||||
return sortedTodos.reduce(
|
||||
(groups, toDo) => {
|
||||
let dateKey;
|
||||
@ -33,9 +39,15 @@ const groupToDosByDate = (toDos: IToDo[]) => {
|
||||
});
|
||||
};
|
||||
|
||||
if (toDo.date === null) {
|
||||
dateKey = "No Date";
|
||||
} else if (isToday(toDo.date)) {
|
||||
const isOverdue = (date: Date) => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return date < today;
|
||||
};
|
||||
|
||||
if (isOverdue(toDo.date) && !toDo.done) {
|
||||
dateKey = "Overdue";
|
||||
} else if (toDo.date === null || isToday(toDo.date)) {
|
||||
dateKey = "Today";
|
||||
} else if (isTomorrow(toDo.date)) {
|
||||
dateKey = "Tomorrow";
|
||||
@ -45,7 +57,8 @@ const groupToDosByDate = (toDos: IToDo[]) => {
|
||||
dateKey = "Next 30 Days";
|
||||
subDateKey = format(toDo.date, "MMM d");
|
||||
} else {
|
||||
return groups;
|
||||
dateKey = "Later";
|
||||
subDateKey = format(toDo.date, "MMM d, yyyy");
|
||||
}
|
||||
|
||||
if (!groups[dateKey]) {
|
||||
@ -55,7 +68,7 @@ const groupToDosByDate = (toDos: IToDo[]) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (dateKey === "Next 30 Days" && subDateKey) {
|
||||
if ((dateKey === "Next 30 Days" || dateKey === "Later") && subDateKey) {
|
||||
if (!groups[dateKey].subgroups[subDateKey]) {
|
||||
groups[dateKey].subgroups[subDateKey] = [];
|
||||
}
|
||||
@ -77,15 +90,23 @@ const groupToDosByDate = (toDos: IToDo[]) => {
|
||||
|
||||
const filterToDosByUser = (toDos: IToDo[], uid: string | undefined) => {
|
||||
if (!uid) return [];
|
||||
return toDos.filter((todo) =>
|
||||
todo.assignees?.includes(uid)
|
||||
);
|
||||
return toDos.filter((todo) => todo.assignees?.includes(uid));
|
||||
};
|
||||
|
||||
const SingleUserChoreList = ({ user }: { user: UserProfile }) => {
|
||||
const { toDos } = useToDosContext();
|
||||
const userTodos = filterToDosByUser(toDos, user.uid);
|
||||
const groupedToDos = groupToDosByDate(userTodos);
|
||||
const [localTodos, setLocalTodos] = useState([]);
|
||||
const [groupedToDos, setGroupedToDos] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const userTodos = filterToDosByUser(toDos, user.uid);
|
||||
setLocalTodos(userTodos);
|
||||
}, [toDos, user]);
|
||||
|
||||
useEffect(() => {
|
||||
const grouped = groupToDosByDate(localTodos);
|
||||
setGroupedToDos(grouped);
|
||||
}, [localTodos]);
|
||||
|
||||
const [expandedGroups, setExpandedGroups] = useState<{
|
||||
[key: string]: boolean;
|
||||
@ -101,14 +122,17 @@ const SingleUserChoreList = ({ user }: { user: UserProfile }) => {
|
||||
};
|
||||
|
||||
const noDateToDos = groupedToDos["No Date"]?.items || [];
|
||||
const datedToDos = Object.keys(groupedToDos).filter(
|
||||
(key) => key !== "No Date"
|
||||
);
|
||||
const datedToDos = Object.keys(groupedToDos)
|
||||
.filter((key) => key !== "No Date")
|
||||
.sort((a, b) => {
|
||||
const order = ["Overdue", "Today", "Tomorrow", "Next 7 Days", "Next 30 Days", "Later"];
|
||||
return order.indexOf(a) - order.indexOf(b);
|
||||
});
|
||||
|
||||
const renderTodoGroup = (dateKey: string) => {
|
||||
const isExpanded = expandedGroups[dateKey] || false;
|
||||
|
||||
if (dateKey === "Next 30 Days") {
|
||||
if (dateKey === "Next 30 Days" || dateKey === "Later") {
|
||||
const subgroups = Object.entries(groupedToDos[dateKey].subgroups).sort(
|
||||
([dateA], [dateB]) => {
|
||||
const dateAObj = new Date(dateA);
|
||||
@ -172,7 +196,12 @@ const SingleUserChoreList = ({ user }: { user: UserProfile }) => {
|
||||
</View>
|
||||
|
||||
{sortedItems.map((item) => (
|
||||
<ToDoItem key={item.id} item={item} is7Days={false} />
|
||||
<ToDoItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
is7Days={false}
|
||||
localTodos={localTodos}
|
||||
setLocalTodos={setLocalTodos}/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
@ -221,6 +250,8 @@ const SingleUserChoreList = ({ user }: { user: UserProfile }) => {
|
||||
key={item.id}
|
||||
item={item}
|
||||
is7Days={dateKey === "Next 7 Days"}
|
||||
localTodos={localTodos}
|
||||
setLocalTodos={setLocalTodos}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
@ -229,41 +260,48 @@ const SingleUserChoreList = ({ user }: { user: UserProfile }) => {
|
||||
|
||||
return (
|
||||
<View
|
||||
marginB-402
|
||||
marginT-10
|
||||
paddingH-10
|
||||
backgroundColor="#f9f8f7"
|
||||
style={{ minHeight: 800, borderRadius: 9.11 }}
|
||||
style={{
|
||||
minHeight: 600,
|
||||
maxHeight: 600,
|
||||
borderRadius: 9.11,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
width={355}
|
||||
>
|
||||
{noDateToDos.length > 0 && (
|
||||
<View key="No Date">
|
||||
<View row spread paddingH-19 marginB-12>
|
||||
<Text
|
||||
text70
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Unscheduled
|
||||
</Text>
|
||||
<AntDesign
|
||||
name={expandNoDate ? "caretdown" : "caretright"}
|
||||
size={24}
|
||||
color="#fd1575"
|
||||
onPress={() => {
|
||||
setExpandNoDate(!expandNoDate);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{expandNoDate &&
|
||||
noDateToDos
|
||||
.sort((a, b) => Number(a.done) - Number(b.done))
|
||||
.map((item) => <ToDoItem key={item.id} item={item} />)}
|
||||
</View>
|
||||
)}
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<View paddingH-10 paddingB-90>
|
||||
{noDateToDos.length > 0 && (
|
||||
<View key="No Date">
|
||||
<View row spread paddingH-19 marginB-12>
|
||||
<Text
|
||||
text70
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Unscheduled
|
||||
</Text>
|
||||
<AntDesign
|
||||
name={expandNoDate ? "caretdown" : "caretright"}
|
||||
size={24}
|
||||
color="#fd1575"
|
||||
onPress={() => {
|
||||
setExpandNoDate(!expandNoDate);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{expandNoDate &&
|
||||
noDateToDos
|
||||
.sort((a, b) => Number(a.done) - Number(b.done))
|
||||
.map((item) => <ToDoItem key={item.id} item={item} localTodos={localTodos} setLocalTodos={setLocalTodos} />)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{datedToDos.map(renderTodoGroup)}
|
||||
{datedToDos.map(renderTodoGroup)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,85 +1,127 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { View, Text } from "react-native-ui-lib";
|
||||
import React, {useEffect, useMemo} from "react";
|
||||
import {Text, View} from "react-native-ui-lib";
|
||||
import * as ScreenOrientation from "expo-screen-orientation";
|
||||
import TabletContainer from "../tablet_components/TabletContainer";
|
||||
import ToDosPage from "../../todos/ToDosPage";
|
||||
import ToDosList from "../../todos/ToDosList";
|
||||
import SingleUserChoreList from "./SingleUserChoreList";
|
||||
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
||||
import { ImageBackground, StyleSheet } from "react-native";
|
||||
import { colorMap } from "@/constants/colorMap";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||
import AddChore from "../../todos/AddChore";
|
||||
import { useAtom } from "jotai";
|
||||
import { selectedUserAtom } from "@/components/pages/calendar/atoms";
|
||||
|
||||
const TabletChoresPage = () => {
|
||||
const { data: users } = useGetFamilyMembers();
|
||||
// Function to lock the screen orientation to landscape
|
||||
const lockScreenOrientation = async () => {
|
||||
await ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
|
||||
);
|
||||
};
|
||||
const {data: users} = useGetFamilyMembers();
|
||||
const { user: currentUser } = useAuthContext();
|
||||
const [selectedUser] = useAtom(selectedUserAtom);
|
||||
|
||||
useEffect(() => {
|
||||
lockScreenOrientation(); // Lock orientation when the component mounts
|
||||
const sortedUsers = useMemo(() => {
|
||||
return users
|
||||
?.filter(member => member.userType !== ProfileType.FAMILY_DEVICE)
|
||||
.sort((a, b) => {
|
||||
if (a.uid === currentUser?.uid) return -1;
|
||||
if (b.uid === currentUser?.uid) return 1;
|
||||
|
||||
return () => {
|
||||
// Optional: Unlock to default when the component unmounts
|
||||
ScreenOrientation.unlockAsync();
|
||||
const typePriority = {
|
||||
[ProfileType.PARENT]: 0,
|
||||
[ProfileType.CHILD]: 1,
|
||||
[ProfileType.CAREGIVER]: 2
|
||||
};
|
||||
|
||||
return typePriority[a.userType] - typePriority[b.userType];
|
||||
});
|
||||
}, [users, currentUser]);
|
||||
|
||||
const lockScreenOrientation = async () => {
|
||||
await ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TabletContainer>
|
||||
<ScrollView horizontal>
|
||||
<View row gap-25 padding-25>
|
||||
{users?.map((user, index) => (
|
||||
<View>
|
||||
<View row centerV>
|
||||
{user.pfp ? (
|
||||
<ImageBackground
|
||||
source={{ uri: user.pfp }}
|
||||
style={styles.pfp}
|
||||
borderRadius={13.33}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
center
|
||||
style={styles.pfp}
|
||||
backgroundColor={user.eventColor || "#00a8b6"}
|
||||
>
|
||||
<Text color="white">
|
||||
{user.firstName.at(0)}
|
||||
{user.lastName.at(0)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.name} marginL-15>
|
||||
{user.firstName}
|
||||
</Text>
|
||||
<Text style={[styles.name, { color: "#9b9b9b" }]} marginL-5>
|
||||
({user.userType})
|
||||
</Text>
|
||||
</View>
|
||||
<SingleUserChoreList user={user} />
|
||||
useEffect(() => {
|
||||
lockScreenOrientation();
|
||||
|
||||
return () => {
|
||||
ScreenOrientation.unlockAsync();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const capitalizeFirstLetter = (str: string) => {
|
||||
if (!str) return "";
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||
};
|
||||
|
||||
return (
|
||||
<TabletContainer>
|
||||
<ScrollView horizontal>
|
||||
<View row gap-25 padding-25>
|
||||
{sortedUsers
|
||||
?.filter((member) =>
|
||||
!selectedUser ||
|
||||
selectedUser.uid === 'family-view' ||
|
||||
selectedUser.uid === member.uid
|
||||
)
|
||||
.map((user, index) => (
|
||||
<View key={index}>
|
||||
<View row centerV>
|
||||
{user.pfp ? (
|
||||
<ImageBackground
|
||||
source={{ uri: user.pfp }}
|
||||
style={styles.pfp}
|
||||
imageStyle={(user.eventColor && {
|
||||
borderWidth: 2,
|
||||
borderColor: user.eventColor,
|
||||
}) || undefined}
|
||||
borderRadius={13.33}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
center
|
||||
style={styles.pfp}
|
||||
backgroundColor={user.eventColor || "#00a8b6"}
|
||||
>
|
||||
<Text color="white">
|
||||
{user.firstName.at(0)}
|
||||
{user.lastName.at(0)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.name} marginL-15>
|
||||
{user.firstName}
|
||||
</Text>
|
||||
<Text style={[styles.name, { color: "#9b9b9b" }]} marginL-5>
|
||||
({capitalizeFirstLetter(user.userType)})
|
||||
</Text>
|
||||
</View>
|
||||
<SingleUserChoreList user={user} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<View style={styles.addBtn}>
|
||||
<AddChore />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</TabletContainer>
|
||||
);
|
||||
</TabletContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pfp: {
|
||||
width: 46.74,
|
||||
aspectRatio: 1,
|
||||
borderRadius: 13.33,
|
||||
},
|
||||
name: {
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
fontSize: 22.43,
|
||||
color: "#2c2c2c",
|
||||
},
|
||||
pfp: {
|
||||
width: 46.74,
|
||||
aspectRatio: 1,
|
||||
borderRadius: 13.33,
|
||||
},
|
||||
name: {
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
fontSize: 22.43,
|
||||
color: "#2c2c2c",
|
||||
},
|
||||
addBtn: {
|
||||
position: 'absolute',
|
||||
bottom: 50,
|
||||
right: 220
|
||||
}
|
||||
});
|
||||
|
||||
export default TabletChoresPage;
|
||||
export default TabletChoresPage;
|
||||
@ -1,6 +1,6 @@
|
||||
import { View, Text, ViewProps } from "react-native-ui-lib";
|
||||
import React, { ReactNode } from "react";
|
||||
import { Dimensions, StyleSheet } from "react-native";
|
||||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
import { Dimensions, StyleSheet, useWindowDimensions } from "react-native";
|
||||
import UsersList from "./UsersList";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
|
||||
@ -8,12 +8,52 @@ interface TabletContainerProps extends ViewProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const { width, height } = Dimensions.get("window");
|
||||
|
||||
const TabletContainer: React.FC<TabletContainerProps> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const window = useWindowDimensions();
|
||||
const [containerWidth, setContainerWidth] = useState(Dimensions.get('window').width);
|
||||
const [containerHeight, setContainerHeight] = useState(Dimensions.get('window').height);
|
||||
|
||||
// Update dimensions on mount and when window size changes
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
setContainerWidth(window.width);
|
||||
setContainerHeight(window.height);
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
|
||||
// Force a second update after a brief delay to handle any initial rendering issues
|
||||
const timer = setTimeout(updateDimensions, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [window.width, window.height]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: "white",
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderTopColor: "#a9a9a9",
|
||||
width: containerWidth,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
calendarContainer: {
|
||||
backgroundColor: "white",
|
||||
height: containerHeight,
|
||||
width: containerWidth * 0.89,
|
||||
},
|
||||
profilesContainer: {
|
||||
width: containerWidth * 0.11,
|
||||
height: containerHeight,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: "#a9a9a9",
|
||||
backgroundColor: "white",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View row>
|
||||
@ -28,25 +68,4 @@ const TabletContainer: React.FC<TabletContainerProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: "white",
|
||||
flex: 1,
|
||||
borderTopColor: "#a9a9a9",
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
calendarContainer: {
|
||||
backgroundColor: "white",
|
||||
height: height,
|
||||
width: width * 0.89,
|
||||
},
|
||||
profilesContainer: {
|
||||
width: width * 0.11,
|
||||
height: height,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: "#a9a9a9",
|
||||
backgroundColor: "white",
|
||||
},
|
||||
});
|
||||
|
||||
export default TabletContainer;
|
||||
export default TabletContainer;
|
||||
@ -1,27 +1,86 @@
|
||||
import { View, Text } from "react-native-ui-lib";
|
||||
import React from "react";
|
||||
import { View, Text, TouchableOpacity } from "react-native-ui-lib";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
||||
import { ImageBackground, StyleSheet } from "react-native";
|
||||
import { colorMap } from "@/constants/colorMap";
|
||||
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||
import { useAtom } from "jotai";
|
||||
import { selectedUserAtom } from "@/components/pages/calendar/atoms";
|
||||
|
||||
type UserProfile = {
|
||||
uid: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
userType: string;
|
||||
eventColor: string;
|
||||
pfp?: string;
|
||||
};
|
||||
|
||||
const UsersList = () => {
|
||||
const { data: familyMembers } = useGetFamilyMembers();
|
||||
const { user: currentUser } = useAuthContext();
|
||||
const { data: familyMembers, refetch: refetchFamilyMembers } =
|
||||
useGetFamilyMembers();
|
||||
const [selectedUser, setSelectedUser] = useAtom(selectedUserAtom);
|
||||
|
||||
useEffect(() => {
|
||||
refetchFamilyMembers();
|
||||
}, []);
|
||||
|
||||
const sortedMembers = useMemo(() => {
|
||||
const filtered = familyMembers?.filter(
|
||||
(member) => member.userType !== ProfileType.FAMILY_DEVICE
|
||||
) || [];
|
||||
|
||||
const currentUserData = filtered.find(m => m.uid === currentUser?.uid);
|
||||
const parents = filtered.filter(m => m.userType === ProfileType.PARENT && m.uid !== currentUser?.uid);
|
||||
const children = filtered.filter(m => m.userType === ProfileType.CHILD && m.uid !== currentUser?.uid);
|
||||
const caregivers = filtered.filter(m => m.userType === ProfileType.CAREGIVER && m.uid !== currentUser?.uid);
|
||||
|
||||
const familyViewOption: UserProfile = {
|
||||
uid: 'family-view',
|
||||
firstName: 'Family',
|
||||
lastName: 'View',
|
||||
userType: 'Family View',
|
||||
eventColor: colorMap.pink
|
||||
};
|
||||
|
||||
return currentUserData
|
||||
? [currentUserData, ...parents, ...children, familyViewOption, ...caregivers]
|
||||
: [...parents, ...children, familyViewOption, ...caregivers];
|
||||
}, [familyMembers, currentUser]);
|
||||
|
||||
const capitalizeFirstLetter = (str: string) => {
|
||||
if (!str) return '';
|
||||
if (!str) return "";
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||
};
|
||||
|
||||
return (
|
||||
<View centerH paddingT-10>
|
||||
{familyMembers?.map((member, index) => (
|
||||
<>
|
||||
<View centerH paddingT-10 marginB-70>
|
||||
{sortedMembers?.map((member, index) => (
|
||||
<TouchableOpacity
|
||||
key={member.uid}
|
||||
onPress={() => {
|
||||
if (member.uid === 'family-view') {
|
||||
setSelectedUser(null);
|
||||
} else {
|
||||
setSelectedUser(selectedUser?.uid === member.uid ? null : member);
|
||||
}
|
||||
}}
|
||||
style={[
|
||||
styles.memberContainer,
|
||||
selectedUser?.uid === member.uid && styles.selectedMember,
|
||||
]}
|
||||
>
|
||||
{member.pfp ? (
|
||||
<ImageBackground
|
||||
key={index}
|
||||
source={{ uri: member.pfp }}
|
||||
style={styles.pfp}
|
||||
borderRadius={100}
|
||||
imageStyle={{
|
||||
borderRadius: 200,
|
||||
borderWidth: 2,
|
||||
borderColor: member.eventColor || colorMap.teal
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
@ -29,17 +88,18 @@ const UsersList = () => {
|
||||
style={styles.pfp}
|
||||
center
|
||||
backgroundColor={member.eventColor || colorMap.teal}
|
||||
children={
|
||||
<Text color="white">
|
||||
{member.firstName.at(0)}
|
||||
{member.lastName.at(0)}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Text color="white">
|
||||
{member.firstName.charAt(0)}
|
||||
{member.lastName.charAt(0)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.fName}>{member.firstName}</Text>
|
||||
<Text style={styles.role}>{capitalizeFirstLetter(member.userType)}</Text>
|
||||
</>
|
||||
<Text style={styles.role}>
|
||||
{capitalizeFirstLetter(member.userType)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
@ -62,6 +122,13 @@ const styles = StyleSheet.create({
|
||||
color: "#9b9b9b",
|
||||
marginBottom: 20,
|
||||
},
|
||||
memberContainer: {
|
||||
alignItems: "center",
|
||||
marginBottom: 20,
|
||||
},
|
||||
selectedMember: {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default UsersList;
|
||||
export default UsersList;
|
||||
@ -13,8 +13,8 @@ interface IAddBrainDumpProps {
|
||||
}
|
||||
|
||||
const AddBrainDump = ({
|
||||
addBrainDumpProps,
|
||||
}: {
|
||||
addBrainDumpProps,
|
||||
}: {
|
||||
addBrainDumpProps: IAddBrainDumpProps;
|
||||
}) => {
|
||||
const {addBrainDump} = useBrainDumpContext();
|
||||
@ -22,11 +22,11 @@ const AddBrainDump = ({
|
||||
const [dumpDesc, setDumpDesc] = useState<string>("");
|
||||
const {width} = Dimensions.get("screen");
|
||||
|
||||
|
||||
// Refs for the two TextFields
|
||||
const descriptionRef = useRef<TextFieldRef>(null);
|
||||
const titleRef = useRef<TextFieldRef>(null);
|
||||
|
||||
const isTitleValid = dumpTitle.trim().length >= 3;
|
||||
|
||||
useEffect(() => {
|
||||
setDumpDesc("");
|
||||
setDumpTitle("");
|
||||
@ -40,9 +40,9 @@ const AddBrainDump = ({
|
||||
}
|
||||
}, [addBrainDumpProps.isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@ -69,18 +69,17 @@ const AddBrainDump = ({
|
||||
<Button
|
||||
color="#05a8b6"
|
||||
label="Save"
|
||||
style={styles.topBtn}
|
||||
style={[styles.topBtn, !isTitleValid && styles.disabledBtn]}
|
||||
disabled={!isTitleValid}
|
||||
onPress={() => {
|
||||
addBrainDump({
|
||||
|
||||
id: 99,
|
||||
|
||||
title: dumpTitle.trimEnd().trimStart(),
|
||||
|
||||
description: dumpDesc.trimEnd().trimStart(),
|
||||
|
||||
});
|
||||
addBrainDumpProps.setIsVisible(false);
|
||||
if (isTitleValid) {
|
||||
addBrainDump({
|
||||
id: '99',
|
||||
title: dumpTitle.trim(),
|
||||
description: dumpDesc.trim(),
|
||||
});
|
||||
addBrainDumpProps.setIsVisible(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@ -94,11 +93,10 @@ const AddBrainDump = ({
|
||||
setDumpTitle(text);
|
||||
}}
|
||||
onSubmitEditing={() => {
|
||||
// Move focus to the description field
|
||||
descriptionRef.current?.focus();
|
||||
}}
|
||||
style={styles.title}
|
||||
blurOnSubmit={false} // Keep the keyboard open when moving focus
|
||||
blurOnSubmit={false}
|
||||
returnKeyType="next"
|
||||
/>
|
||||
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20/>
|
||||
@ -125,28 +123,31 @@ const AddBrainDump = ({
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dialogContainer: {
|
||||
borderTopRightRadius: 15,
|
||||
borderTopLeftRadius: 15,
|
||||
backgroundColor: "white",
|
||||
padding: 0,
|
||||
paddingTop: 3,
|
||||
margin: 0,
|
||||
},
|
||||
topBtns: {},
|
||||
topBtn: {
|
||||
backgroundColor: "white",
|
||||
color: "#05a8b6",
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
fontFamily: "Manrope_500Medium",
|
||||
},
|
||||
description: {
|
||||
fontFamily: "Manrope_400Regular",
|
||||
fontSize: 14,
|
||||
textAlignVertical: "top",
|
||||
},
|
||||
dialogContainer: {
|
||||
borderTopRightRadius: 15,
|
||||
borderTopLeftRadius: 15,
|
||||
backgroundColor: "white",
|
||||
padding: 0,
|
||||
paddingTop: 3,
|
||||
margin: 0,
|
||||
},
|
||||
topBtns: {},
|
||||
topBtn: {
|
||||
backgroundColor: "white",
|
||||
color: "#05a8b6",
|
||||
},
|
||||
disabledBtn: {
|
||||
opacity: 0.2,
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
fontFamily: "Manrope_500Medium",
|
||||
},
|
||||
description: {
|
||||
fontFamily: "Manrope_400Regular",
|
||||
fontSize: 14,
|
||||
textAlignVertical: "top",
|
||||
},
|
||||
});
|
||||
|
||||
export default AddBrainDump;
|
||||
export default AddBrainDump;
|
||||
@ -3,14 +3,17 @@ import React, {useState} from "react";
|
||||
import {Button, Text, TextField, View} from "react-native-ui-lib";
|
||||
import DumpList from "./DumpList";
|
||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
||||
import {Feather, MaterialIcons} from "@expo/vector-icons";
|
||||
import {Feather} from "@expo/vector-icons";
|
||||
import AddBrainDump from "./AddBrainDump";
|
||||
import LinearGradient from "react-native-linear-gradient";
|
||||
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||
import * as Device from 'expo-device'
|
||||
import {DeviceType} from 'expo-device'
|
||||
|
||||
const BrainDumpPage = () => {
|
||||
const [searchText, setSearchText] = useState<string>("");
|
||||
const [isAddVisible, setIsAddVisible] = useState<boolean>(false);
|
||||
const isTablet: boolean = Device.deviceType === DeviceType.TABLET;
|
||||
|
||||
return (
|
||||
<View height={"100%"}>
|
||||
@ -19,7 +22,7 @@ const BrainDumpPage = () => {
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
<View marginH-25>
|
||||
<View marginH-25 marginT-20 style={isTablet ? {alignItems: 'center'} : undefined}>
|
||||
<HeaderTemplate
|
||||
message={"Welcome to your notes!"}
|
||||
isWelcome={false}
|
||||
@ -31,7 +34,7 @@ const BrainDumpPage = () => {
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<View>
|
||||
<View style={isTablet ? {maxWidth: 390} : undefined}>
|
||||
<View style={styles.searchField} centerV>
|
||||
<TextField
|
||||
value={searchText}
|
||||
@ -78,6 +81,7 @@ const BrainDumpPage = () => {
|
||||
bottom: -10,
|
||||
borderRadius: 30,
|
||||
backgroundColor: "#fd1775",
|
||||
maxWidth: 450,
|
||||
}}
|
||||
color="white"
|
||||
enableShadow
|
||||
@ -114,6 +118,8 @@ const styles = StyleSheet.create({
|
||||
height: 42,
|
||||
paddingLeft: 10,
|
||||
marginVertical: 20,
|
||||
marginTop: 30,
|
||||
boxShadow: 'inset 0px 0px 37px 0px rgba(239,238,237,1)',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ const BrainDumpItem = (props: { item: IBrainDump }) => {
|
||||
marginV-5
|
||||
paddingH-13
|
||||
paddingV-10
|
||||
style={{ borderRadius: 15, elevation: 2 }}
|
||||
style={{ borderRadius: 15, elevation: 0 }}
|
||||
>
|
||||
<Text
|
||||
text70B
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import { View } from "react-native-ui-lib";
|
||||
import { View, Text } from "react-native-ui-lib";
|
||||
import React from "react";
|
||||
import { useBrainDumpContext } from "@/contexts/DumpContext";
|
||||
import { FlatList } from "react-native";
|
||||
import BrainDumpItem from "./DumpItem";
|
||||
import LinearGradient from "react-native-linear-gradient";
|
||||
|
||||
import { StyleSheet } from "react-native";
|
||||
const DumpList = (props: { searchText: string }) => {
|
||||
const { brainDumps } = useBrainDumpContext();
|
||||
|
||||
const filteredBrainDumps =
|
||||
const sortedDumps =
|
||||
props.searchText.trim() === ""
|
||||
? brainDumps
|
||||
: brainDumps.filter(
|
||||
@ -19,18 +18,17 @@ const DumpList = (props: { searchText: string }) => {
|
||||
.includes(props.searchText.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<View marginB-70>
|
||||
<FlatList
|
||||
style={{ zIndex: -1 }}
|
||||
data={filteredBrainDumps}
|
||||
keyExtractor={(item) => item.title}
|
||||
renderItem={({ item }) => (
|
||||
<BrainDumpItem key={item.title} item={item} />
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View marginB-70>
|
||||
{sortedDumps?.length ? (
|
||||
sortedDumps.map((item) => (
|
||||
<BrainDumpItem key={item.id} item={item} />
|
||||
))) : <Text marginT-20 center style={styles.alert}>You have no notes</Text>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
alert: {fontFamily: "PlusJakartaSans_300Light", fontSize: 20}
|
||||
})
|
||||
export default DumpList;
|
||||
|
||||
@ -145,7 +145,7 @@ const MoveBrainDump = (props: {
|
||||
style={styles.optionsIcon}
|
||||
/>
|
||||
<Text style={styles.optionsReg}>Move to</Text>
|
||||
<Text style={styles.optionsBold}> my to do's</Text>
|
||||
<Text style={styles.optionsBold}> my to dos</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React, {useState} from "react";
|
||||
import {MaterialIcons,} from "@expo/vector-icons";
|
||||
import {Button, Card, Dialog, PanningProvider, Text, View,} from "react-native-ui-lib";
|
||||
import {StyleSheet, TouchableOpacity} from "react-native";
|
||||
import AddChoreDialog from "../todos/AddChoreDialog";
|
||||
@ -11,6 +10,7 @@ import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||
import {useSetAtom} from "jotai";
|
||||
import {selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||
import {addMinutes, roundToNearestMinutes} from "date-fns";
|
||||
|
||||
export const AddEventDialog = () => {
|
||||
const [show, setShow] = useState(false);
|
||||
@ -21,7 +21,8 @@ export const AddEventDialog = () => {
|
||||
const handleOpenManualInputModal = () => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
setSelectedNewEndDate(new Date());
|
||||
const roundedDate = roundToNearestMinutes(new Date(), {nearestTo: 5});
|
||||
setSelectedNewEndDate(roundedDate);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
@ -51,7 +52,7 @@ export const AddEventDialog = () => {
|
||||
onPress={() => setShow(true)}
|
||||
>
|
||||
<View row centerV centerH>
|
||||
<PlusIcon />
|
||||
<PlusIcon/>
|
||||
<Text white style={{fontSize: 16, fontFamily: 'Manrope_600SemiBold', marginLeft: 5}}>
|
||||
New
|
||||
</Text>
|
||||
@ -82,7 +83,7 @@ export const AddEventDialog = () => {
|
||||
paddingVertical: 13,
|
||||
opacity: 0.5
|
||||
}}
|
||||
label="Scan Image"
|
||||
label="Upload Image"
|
||||
labelStyle={styles.btnLabel}
|
||||
onPress={handleScanImageDialog}
|
||||
iconSource={() => (
|
||||
@ -102,26 +103,24 @@ export const AddEventDialog = () => {
|
||||
labelStyle={styles.btnLabel}
|
||||
onPress={handleOpenManualInputModal}
|
||||
iconSource={() => (
|
||||
<CalendarIcon color={"white"} style={styles.btnIcon}/>
|
||||
<CalendarIcon color="white" style={styles.btnIcon}/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled
|
||||
style={{
|
||||
marginBottom: 10,
|
||||
// backgroundColor: "#05a8b6",
|
||||
backgroundColor: "#05a8b6",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
paddingVertical: 13,
|
||||
opacity: 0.5
|
||||
paddingVertical: 13
|
||||
}}
|
||||
label="Add To Do"
|
||||
labelStyle={styles.btnLabel}
|
||||
onPress={() => setChoreDialogVisible(true)}
|
||||
iconSource={() => (
|
||||
<NavToDosIcon
|
||||
color={"white"}
|
||||
color="white"
|
||||
width={23}
|
||||
style={styles.btnIcon}
|
||||
/>
|
||||
|
||||
27
components/pages/calendar/CalendarController.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import {useAtomValue} from 'jotai';
|
||||
import {selectedDateAtom} from '@/components/pages/calendar/atoms';
|
||||
import {FlashList} from "@shopify/flash-list";
|
||||
import {useDidUpdate} from "react-native-ui-lib/src/hooks";
|
||||
|
||||
interface CalendarControllerProps {
|
||||
scrollViewRef: React.RefObject<FlashList<any>>;
|
||||
centerMonthIndex: number;
|
||||
}
|
||||
|
||||
export const CalendarController: React.FC<CalendarControllerProps> = (
|
||||
{
|
||||
scrollViewRef,
|
||||
centerMonthIndex
|
||||
}) => {
|
||||
const selectedDate = useAtomValue(selectedDateAtom);
|
||||
|
||||
useDidUpdate(() => {
|
||||
scrollViewRef.current?.scrollToIndex({
|
||||
index: centerMonthIndex,
|
||||
animated: false
|
||||
})
|
||||
}, [selectedDate, centerMonthIndex]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,130 +1,181 @@
|
||||
import React, { memo } from "react";
|
||||
import {
|
||||
Button,
|
||||
Picker,
|
||||
PickerModes,
|
||||
SegmentedControl,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native-ui-lib";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { modeMap, months } from "./constants";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { useAtom } from "jotai";
|
||||
import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms";
|
||||
import { format, isSameDay } from "date-fns";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import React, {memo, useCallback, useMemo, useState} from "react";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {Button, Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib";
|
||||
import {MaterialIcons} from "@expo/vector-icons";
|
||||
import {useAtom} from "jotai";
|
||||
import {format} from "date-fns";
|
||||
import * as Device from "expo-device";
|
||||
import {useIsFetching} from "@tanstack/react-query";
|
||||
|
||||
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import {months} from "./constants";
|
||||
import RefreshButton from "@/components/shared/RefreshButton";
|
||||
import {useCalSync} from "@/hooks/useCalSync";
|
||||
|
||||
type ViewMode = "day" | "week" | "month" | "3days";
|
||||
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
|
||||
const SEGMENTS = isTablet
|
||||
? [{label: "D"}, {label: "W"}, {label: "M"}]
|
||||
: [{label: "D"}, {label: "3D"}, {label: "M"}];
|
||||
|
||||
const MODE_MAP = {
|
||||
tablet: ["day", "week", "month"],
|
||||
mobile: ["day", "3days", "month"]
|
||||
} as const;
|
||||
|
||||
export const CalendarHeader = memo(() => {
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const [mode, setMode] = useAtom(modeAtom);
|
||||
const { profileData } = useAuthContext();
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const [mode, setMode] = useAtom(modeAtom);
|
||||
const [tempIndex, setTempIndex] = useState<number | null>(null);
|
||||
|
||||
const handleSegmentChange = (index: number) => {
|
||||
const selectedMode = modeMap.get(index);
|
||||
if (selectedMode) {
|
||||
setTimeout(() => {
|
||||
setMode(selectedMode as "day" | "week" | "month");
|
||||
}, 150);
|
||||
}
|
||||
};
|
||||
const {resyncAllCalendars, isSyncing} = useCalSync();
|
||||
const isFetching = useIsFetching({queryKey: ['events']}) > 0;
|
||||
|
||||
const handleMonthChange = (month: string) => {
|
||||
const currentDay = selectedDate.getDate();
|
||||
const currentYear = selectedDate.getFullYear();
|
||||
const newMonthIndex = months.indexOf(month);
|
||||
const isLoading = useMemo(() => isSyncing || isFetching, [isSyncing, isFetching]);
|
||||
|
||||
const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
|
||||
setSelectedDate(updatedDate);
|
||||
};
|
||||
const handleSegmentChange = useCallback((index: number) => {
|
||||
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
|
||||
const selectedMode = modes[index] as ViewMode;
|
||||
|
||||
const isSelectedDateToday = isSameDay(selectedDate, new Date());
|
||||
setTempIndex(index);
|
||||
setTimeout(() => {
|
||||
setMode(selectedMode);
|
||||
setTempIndex(null);
|
||||
}, 150);
|
||||
}, [setMode]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
backgroundColor: "white",
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<View row centerV gap-3>
|
||||
<Text style={{ fontFamily: "Manrope_500Medium", fontSize: 17 }}>
|
||||
{selectedDate.getFullYear()}
|
||||
</Text>
|
||||
<Picker
|
||||
value={months[selectedDate.getMonth()]}
|
||||
placeholder={"Select Month"}
|
||||
style={{ fontFamily: "Manrope_500Medium", fontSize: 17, width: 85 }}
|
||||
mode={PickerModes.SINGLE}
|
||||
onChange={(itemValue) => handleMonthChange(itemValue as string)}
|
||||
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"} />}
|
||||
topBarProps={{
|
||||
title: selectedDate.getFullYear().toString(),
|
||||
titleStyle: { fontFamily: "Manrope_500Medium", fontSize: 17 },
|
||||
}}
|
||||
>
|
||||
{months.map((month) => (
|
||||
<Picker.Item key={month} label={month} value={month} />
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
const handleMonthChange = useCallback((month: string) => {
|
||||
const newMonthIndex = months.indexOf(month);
|
||||
const updatedDate = new Date(
|
||||
selectedDate.getFullYear(),
|
||||
newMonthIndex,
|
||||
selectedDate.getDate()
|
||||
);
|
||||
setSelectedDate(updatedDate);
|
||||
}, [selectedDate, setSelectedDate]);
|
||||
|
||||
<View row centerV>
|
||||
{!isSelectedDateToday && (
|
||||
<Button
|
||||
size={"xSmall"}
|
||||
marginR-0
|
||||
avoidInnerPadding
|
||||
style={{
|
||||
borderRadius: 50,
|
||||
backgroundColor: "white",
|
||||
borderWidth: 0.7,
|
||||
borderColor: "#dadce0",
|
||||
height: 30,
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
labelStyle={{
|
||||
fontSize: 12,
|
||||
color: "black",
|
||||
fontFamily: "Manrope_500Medium",
|
||||
}}
|
||||
label={format(new Date(), "dd/MM/yyyy")}
|
||||
onPress={() => {
|
||||
setSelectedDate(new Date());
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
const handleRefresh = useCallback(async () => {
|
||||
try {
|
||||
await resyncAllCalendars();
|
||||
} catch (error) {
|
||||
console.error("Refresh failed:", error);
|
||||
}
|
||||
}, [resyncAllCalendars]);
|
||||
|
||||
<View>
|
||||
<SegmentedControl
|
||||
segments={[{ label: "D" }, { label: "W" }, { label: "M" }]}
|
||||
backgroundColor="#ececec"
|
||||
inactiveColor="#919191"
|
||||
activeBackgroundColor="#ea156c"
|
||||
activeColor="white"
|
||||
outlineColor="white"
|
||||
outlineWidth={3}
|
||||
segmentLabelStyle={styles.segmentslblStyle}
|
||||
onChangeIndex={handleSegmentChange}
|
||||
initialIndex={mode === "day" ? 0 : mode === "week" ? 1 : 2}
|
||||
/>
|
||||
const getInitialIndex = useCallback(() => {
|
||||
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
|
||||
//@ts-ignore
|
||||
return modes.indexOf(mode);
|
||||
}, [mode]);
|
||||
|
||||
const renderMonthPicker = () => (
|
||||
<>
|
||||
{isTablet && <View flexG/>}
|
||||
<View row centerV gap-1 flexS>
|
||||
{isTablet && (
|
||||
<Text style={styles.yearText}>
|
||||
{selectedDate.getFullYear()}
|
||||
</Text>
|
||||
)}
|
||||
<Picker
|
||||
value={months[selectedDate.getMonth()]}
|
||||
placeholder="Select Month"
|
||||
style={styles.monthPicker}
|
||||
mode={PickerModes.SINGLE}
|
||||
onChange={value => handleMonthChange(value as string)}
|
||||
trailingAccessory={<MaterialIcons name="keyboard-arrow-down"/>}
|
||||
topBarProps={{
|
||||
title: selectedDate.getFullYear().toString(),
|
||||
titleStyle: styles.yearText,
|
||||
}}
|
||||
>
|
||||
{months.map(month => (
|
||||
<Picker.Item key={month} label={month} value={month}/>
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container} flexG centerV>
|
||||
{mode !== "month" ? renderMonthPicker() : <View flexG/>}
|
||||
|
||||
<View row centerV flexS>
|
||||
<Button
|
||||
size="xSmall"
|
||||
marginR-1
|
||||
avoidInnerPadding
|
||||
style={styles.todayButton}
|
||||
onPress={() => setSelectedDate(new Date())}
|
||||
>
|
||||
<MaterialIcons name="calendar-today" size={30} color="#5f6368"/>
|
||||
<Text style={styles.todayDate}>{format(new Date(), "d")}</Text>
|
||||
</Button>
|
||||
|
||||
<View style={styles.segmentContainer}>
|
||||
<SegmentedControl
|
||||
segments={SEGMENTS}
|
||||
backgroundColor="#ececec"
|
||||
inactiveColor="#919191"
|
||||
activeBackgroundColor="#ea156c"
|
||||
activeColor="white"
|
||||
outlineColor="white"
|
||||
outlineWidth={3}
|
||||
segmentLabelStyle={styles.segmentLabel}
|
||||
onChangeIndex={handleSegmentChange}
|
||||
initialIndex={tempIndex ?? getInitialIndex()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<RefreshButton onRefresh={handleRefresh} isSyncing={isLoading}/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
segmentslblStyle: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
});
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingVertical: isTablet ? 8 : 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
paddingLeft: 10
|
||||
},
|
||||
yearText: {
|
||||
fontFamily: "Manrope_500Medium",
|
||||
fontSize: 17,
|
||||
},
|
||||
monthPicker: {
|
||||
fontFamily: "Manrope_500Medium",
|
||||
fontSize: 17,
|
||||
width: 85,
|
||||
},
|
||||
segmentContainer: {
|
||||
maxWidth: 120,
|
||||
height: 40,
|
||||
},
|
||||
segmentLabel: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
todayButton: {
|
||||
backgroundColor: "transparent",
|
||||
borderWidth: 0,
|
||||
height: 30,
|
||||
width: 30,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
todayDate: {
|
||||
position: 'absolute',
|
||||
fontSize: 12,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
color: "#5f6368",
|
||||
top: '30%',
|
||||
},
|
||||
});
|
||||
@ -1,21 +1,15 @@
|
||||
import React from "react";
|
||||
import {View,} from "react-native-ui-lib";
|
||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
||||
import {InnerCalendar} from "@/components/pages/calendar/InnerCalendar";
|
||||
import { View } from "react-native-ui-lib";
|
||||
import { InnerCalendar } from "@/components/pages/calendar/InnerCalendar";
|
||||
|
||||
export default function CalendarPage() {
|
||||
return (
|
||||
<View
|
||||
style={{flex: 1, height: "100%", padding: 10}}
|
||||
paddingH-22
|
||||
paddingT-0
|
||||
>
|
||||
<HeaderTemplate
|
||||
message={"Let's get your week started!"}
|
||||
isWelcome
|
||||
isCalendar={true}
|
||||
/>
|
||||
<InnerCalendar/>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View
|
||||
style={{ flex: 1, height: "100%", padding: 0 }}
|
||||
paddingH-0
|
||||
paddingT-0
|
||||
>
|
||||
<InnerCalendar />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,92 +1,94 @@
|
||||
import { Text, TouchableOpacity, View } from "react-native-ui-lib";
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { useAtom } from "jotai";
|
||||
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
|
||||
import {Text, TouchableOpacity, View} from "react-native-ui-lib";
|
||||
import React, {useState, useCallback} from "react";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {useAtom} from "jotai";
|
||||
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
|
||||
|
||||
const CalendarViewSwitch = () => {
|
||||
const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom);
|
||||
const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom);
|
||||
const [localState, setLocalState] = useState(isFamilyView);
|
||||
|
||||
return (
|
||||
<View
|
||||
row
|
||||
spread
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
borderRadius: 30,
|
||||
backgroundColor: "white",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
// iOS shadow
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
// Android shadow (elevation)
|
||||
elevation: 6,
|
||||
}}
|
||||
centerV
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setIsFamilyView(true);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
centerV
|
||||
centerH
|
||||
height={40}
|
||||
paddingH-15
|
||||
style={isFamilyView ? styles.switchBtnActive : styles.switchBtn}
|
||||
>
|
||||
<Text
|
||||
color={isFamilyView ? "white" : "#a1a1a1"}
|
||||
style={styles.switchTxt}
|
||||
>
|
||||
Family View
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
const handleViewChange = useCallback((newValue: boolean) => {
|
||||
setLocalState(newValue);
|
||||
setTimeout(() => {
|
||||
setIsFamilyView(newValue);
|
||||
}, 150);
|
||||
}, [setIsFamilyView]);
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setIsFamilyView(false);
|
||||
}}
|
||||
>
|
||||
return (
|
||||
<View
|
||||
centerV
|
||||
centerH
|
||||
height={40}
|
||||
paddingH-15
|
||||
style={!isFamilyView ? styles.switchBtnActive : styles.switchBtn}
|
||||
row
|
||||
spread
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
borderRadius: 30,
|
||||
backgroundColor: "white",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {width: 0, height: 2},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 6,
|
||||
}}
|
||||
centerV
|
||||
>
|
||||
<Text
|
||||
color={!isFamilyView ? "white" : "#a1a1a1"}
|
||||
style={styles.switchTxt}
|
||||
>
|
||||
My View
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => handleViewChange(true)}
|
||||
>
|
||||
<View
|
||||
centerV
|
||||
centerH
|
||||
height={40}
|
||||
paddingH-15
|
||||
style={localState ? styles.switchBtnActive : styles.switchBtn}
|
||||
>
|
||||
<Text
|
||||
color={localState ? "white" : "#a1a1a1"}
|
||||
style={styles.switchTxt}
|
||||
>
|
||||
Family View
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => handleViewChange(false)}
|
||||
>
|
||||
<View
|
||||
centerV
|
||||
centerH
|
||||
height={40}
|
||||
paddingH-15
|
||||
style={!localState ? styles.switchBtnActive : styles.switchBtn}
|
||||
>
|
||||
<Text
|
||||
color={!localState ? "white" : "#a1a1a1"}
|
||||
style={styles.switchTxt}
|
||||
>
|
||||
My View
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default CalendarViewSwitch;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
switchBtnActive: {
|
||||
backgroundColor: "#a1a1a1",
|
||||
borderRadius: 50,
|
||||
},
|
||||
switchBtn: {
|
||||
backgroundColor: "white",
|
||||
borderRadius: 50,
|
||||
},
|
||||
switchTxt: {
|
||||
fontSize: 16,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
});
|
||||
switchBtnActive: {
|
||||
backgroundColor: "#a1a1a1",
|
||||
borderRadius: 50,
|
||||
},
|
||||
switchBtn: {
|
||||
backgroundColor: "white",
|
||||
borderRadius: 50,
|
||||
},
|
||||
switchTxt: {
|
||||
fontSize: 16,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
});
|
||||
122
components/pages/calendar/DetailedCalendar.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import React, {useCallback, useMemo, useRef, useState} from "react";
|
||||
import {View} from "react-native-ui-lib";
|
||||
import {DeviceType} from "expo-device";
|
||||
import * as Device from "expo-device";
|
||||
import {CalendarBody, CalendarContainer, CalendarHeader, CalendarKitHandle} from "@howljs/calendar-kit";
|
||||
import {useAtomValue} from "jotai";
|
||||
import {selectedUserAtom} from "@/components/pages/calendar/atoms";
|
||||
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
||||
import {useFormattedEvents} from "@/components/pages/calendar/useFormattedEvents";
|
||||
import {useCalendarControls} from "@/components/pages/calendar/useCalendarControls";
|
||||
import {EventCell} from "@/components/pages/calendar/EventCell";
|
||||
import {DetailedCalendarController} from "@/components/pages/calendar/DetailedCalendarController";
|
||||
|
||||
interface EventCalendarProps {
|
||||
calendarWidth: number;
|
||||
mode: "week" | "month" | "day" | "3days";
|
||||
onLoad?: () => void;
|
||||
}
|
||||
|
||||
const HEADER_PROPS = {
|
||||
dayBarHeight: 60,
|
||||
headerBottomHeight: 20,
|
||||
};
|
||||
|
||||
const BODY_PROPS = {
|
||||
showNowIndicator: true,
|
||||
hourFormat: "h:mm a"
|
||||
};
|
||||
|
||||
const MODE_TO_DAYS = {
|
||||
'week': 7,
|
||||
'3days': 3,
|
||||
'day': 1,
|
||||
'month': 1
|
||||
} as const;
|
||||
|
||||
const getContainerProps = (date: Date, customKey: string) => ({
|
||||
hourWidth: 70,
|
||||
allowPinchToZoom: true,
|
||||
useHaptic: true,
|
||||
scrollToNow: true,
|
||||
initialDate: customKey !== "default" ? customKey : date.toISOString(),
|
||||
});
|
||||
|
||||
const MemoizedEventCell = React.memo(EventCell, (prev, next) => {
|
||||
return prev.event.id === next.event.id &&
|
||||
prev.event.lastModified === next.event.lastModified;
|
||||
});
|
||||
|
||||
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo(({
|
||||
calendarWidth,
|
||||
mode,
|
||||
onLoad
|
||||
}) => {
|
||||
const {profileData} = useAuthContext();
|
||||
const {data: familyMembers} = useGetFamilyMembers();
|
||||
const {data: events} = useGetEvents();
|
||||
const selectedUser = useAtomValue(selectedUserAtom);
|
||||
const calendarRef = useRef<CalendarKitHandle>(null);
|
||||
const [customKey, setCustomKey] = useState("defaultKey");
|
||||
|
||||
const memoizedFamilyMembers = useMemo(() => familyMembers || [], [familyMembers]);
|
||||
const currentDate = useMemo(() => new Date(), []);
|
||||
const containerProps = useMemo(() => getContainerProps(currentDate, customKey), [currentDate, customKey]);
|
||||
|
||||
const {data: formattedEvents} = useFormattedEvents(events ?? [], currentDate, selectedUser);
|
||||
const {
|
||||
handlePressEvent,
|
||||
handlePressCell,
|
||||
debouncedOnDateChanged
|
||||
} = useCalendarControls(events ?? []);
|
||||
|
||||
const getAttendees = useCallback((event: any) => {
|
||||
return memoizedFamilyMembers.filter(member => event?.attendees?.includes(member?.uid!));
|
||||
}, [memoizedFamilyMembers]);
|
||||
|
||||
const renderEvent = useCallback((event: any) => {
|
||||
const attendees = getAttendees(event);
|
||||
return (
|
||||
<MemoizedEventCell
|
||||
event={event}
|
||||
onPress={handlePressEvent}
|
||||
attendees={attendees}
|
||||
/>
|
||||
);
|
||||
}, [getAttendees, handlePressEvent]);
|
||||
|
||||
return (
|
||||
<CalendarContainer
|
||||
ref={calendarRef}
|
||||
{...containerProps}
|
||||
numberOfDays={MODE_TO_DAYS[mode]}
|
||||
calendarWidth={calendarWidth}
|
||||
onDateChanged={debouncedOnDateChanged}
|
||||
firstDay={profileData?.firstDayOfWeek === "Mondays" ? 1 : 0}
|
||||
events={formattedEvents ?? []}
|
||||
onPressEvent={handlePressEvent}
|
||||
onPressBackground={handlePressCell}
|
||||
onLoad={onLoad}
|
||||
>
|
||||
<DetailedCalendarController
|
||||
calendarRef={calendarRef}
|
||||
setCustomKey={setCustomKey}
|
||||
/>
|
||||
<CalendarHeader {...HEADER_PROPS}/>
|
||||
<CalendarBody
|
||||
{...BODY_PROPS}
|
||||
renderEvent={renderEvent}
|
||||
/>
|
||||
{Device.deviceType === DeviceType.TABLET && (
|
||||
<View style={{backgroundColor: 'white', height: '9%', width: '100%'}}/>
|
||||
)}
|
||||
</CalendarContainer>
|
||||
);
|
||||
});
|
||||
|
||||
DetailedCalendar.displayName = 'DetailedCalendar';
|
||||
|
||||
export default DetailedCalendar;
|
||||
38
components/pages/calendar/DetailedCalendarController.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React, {useCallback, useEffect, useRef} from 'react';
|
||||
import {useAtomValue} from 'jotai';
|
||||
import {useAtomCallback} from 'jotai/utils';
|
||||
import {modeAtom, selectedDateAtom} from '@/components/pages/calendar/atoms';
|
||||
import {isToday} from 'date-fns';
|
||||
import {CalendarKitHandle} from "@howljs/calendar-kit";
|
||||
|
||||
interface DetailedCalendarDateControllerProps {
|
||||
calendarRef: React.RefObject<CalendarKitHandle>;
|
||||
setCustomKey: (key: string) => void;
|
||||
}
|
||||
|
||||
export const DetailedCalendarController: React.FC<DetailedCalendarDateControllerProps> = ({
|
||||
calendarRef,
|
||||
setCustomKey
|
||||
}) => {
|
||||
const selectedDate = useAtomValue(selectedDateAtom);
|
||||
const lastSelectedDate = useRef(selectedDate);
|
||||
|
||||
const checkModeAndGoToDate = useAtomCallback(useCallback((get) => {
|
||||
const currentMode = get(modeAtom);
|
||||
if ((selectedDate && isToday(selectedDate)) || currentMode === "month") {
|
||||
if (currentMode === "month") {
|
||||
setCustomKey(selectedDate.toISOString());
|
||||
}
|
||||
calendarRef?.current?.goToDate({date: selectedDate});
|
||||
}
|
||||
}, [selectedDate, calendarRef, setCustomKey]));
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate !== lastSelectedDate.current) {
|
||||
checkModeAndGoToDate();
|
||||
lastSelectedDate.current = selectedDate;
|
||||
}
|
||||
}, [selectedDate, checkModeAndGoToDate]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,337 +1,111 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Calendar } from "react-native-big-calendar";
|
||||
import { ActivityIndicator, StyleSheet, View, ViewStyle } from "react-native";
|
||||
import { useGetEvents } from "@/hooks/firebase/useGetEvents";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import {
|
||||
editVisibleAtom,
|
||||
eventForEditAtom,
|
||||
isAllDayAtom,
|
||||
modeAtom,
|
||||
selectedDateAtom,
|
||||
selectedNewEventDateAtom,
|
||||
} from "@/components/pages/calendar/atoms";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { CalendarEvent } from "@/components/pages/calendar/interfaces";
|
||||
import { Text } from "react-native-ui-lib";
|
||||
import { addDays, compareAsc, isWithinInterval, subDays } from "date-fns";
|
||||
import React from 'react';
|
||||
import {StyleSheet, View, ActivityIndicator} from 'react-native';
|
||||
import {Text} from 'react-native-ui-lib';
|
||||
import Animated, {
|
||||
withTiming,
|
||||
useAnimatedStyle,
|
||||
FadeOut,
|
||||
useSharedValue,
|
||||
} from 'react-native-reanimated';
|
||||
import {useGetEvents} from '@/hooks/firebase/useGetEvents';
|
||||
import {useCalSync} from '@/hooks/useCalSync';
|
||||
import {useSyncEvents} from '@/hooks/useSyncOnScroll';
|
||||
import {useAtom} from 'jotai';
|
||||
import {modeAtom} from './atoms';
|
||||
import {MonthCalendar} from "@/components/pages/calendar/MonthCalendar";
|
||||
import DetailedCalendar from "@/components/pages/calendar/DetailedCalendar";
|
||||
import * as Device from "expo-device";
|
||||
|
||||
export type CalendarMode = 'month' | 'day' | '3days' | 'week';
|
||||
|
||||
interface EventCalendarProps {
|
||||
calendarHeight: number;
|
||||
// WAS USED FOR SCROLLABLE CALENDARS, PERFORMANCE WAS NOT OPTIMAL
|
||||
calendarWidth: number;
|
||||
calendarWidth: number;
|
||||
}
|
||||
|
||||
const getTotalMinutes = () => {
|
||||
const date = new Date();
|
||||
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
|
||||
};
|
||||
export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) => {
|
||||
const {isLoading} = useGetEvents();
|
||||
const [mode] = useAtom<CalendarMode>(modeAtom);
|
||||
const {isSyncing} = useSyncEvents();
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
const isCalendarReady = useSharedValue(false);
|
||||
useCalSync();
|
||||
|
||||
export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
({ calendarHeight }) => {
|
||||
const { data: events, isLoading } = useGetEvents();
|
||||
const { profileData } = useAuthContext();
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const [mode, setMode] = useAtom(modeAtom);
|
||||
|
||||
const setEditVisible = useSetAtom(editVisibleAtom);
|
||||
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
|
||||
const setEventForEdit = useSetAtom(eventForEditAtom);
|
||||
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
||||
|
||||
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
||||
|
||||
const todaysDate = new Date();
|
||||
|
||||
const handlePressEvent = useCallback(
|
||||
(event: CalendarEvent) => {
|
||||
if (mode === "day" || mode === "week") {
|
||||
setEditVisible(true);
|
||||
console.log({ event });
|
||||
setEventForEdit(event);
|
||||
} else {
|
||||
setMode("day");
|
||||
setSelectedDate(event.start);
|
||||
}
|
||||
},
|
||||
[setEditVisible, setEventForEdit, mode]
|
||||
);
|
||||
|
||||
const handlePressCell = useCallback(
|
||||
(date: Date) => {
|
||||
if (mode === "day" || mode === "week") {
|
||||
setSelectedNewEndDate(date);
|
||||
} else {
|
||||
setMode("day");
|
||||
setSelectedDate(date);
|
||||
}
|
||||
},
|
||||
[mode, setSelectedNewEndDate, setSelectedDate]
|
||||
);
|
||||
|
||||
const handlePressDayHeader = useCallback(
|
||||
(date: Date) => {
|
||||
setIsAllDay(true);
|
||||
console.log(isAllDay);
|
||||
setSelectedNewEndDate(date);
|
||||
setEditVisible(true);
|
||||
},
|
||||
[setSelectedNewEndDate]
|
||||
);
|
||||
|
||||
const handleSwipeEnd = useCallback(
|
||||
(date: Date) => {
|
||||
setSelectedDate(date);
|
||||
},
|
||||
[setSelectedDate]
|
||||
);
|
||||
|
||||
const memoizedEventCellStyle = useCallback(
|
||||
(event: CalendarEvent) => ({ backgroundColor: event.eventColor }),
|
||||
[]
|
||||
);
|
||||
|
||||
const memoizedWeekStartsOn = useMemo(
|
||||
() => (profileData?.firstDayOfWeek === "Mondays" ? 1 : 0),
|
||||
[profileData]
|
||||
);
|
||||
|
||||
console.log({
|
||||
memoizedWeekStartsOn,
|
||||
profileData: profileData?.firstDayOfWeek,
|
||||
});
|
||||
|
||||
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
||||
return (
|
||||
date1.getDate() === date2.getDate() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getFullYear() === date2.getFullYear()
|
||||
);
|
||||
const handleRenderComplete = React.useCallback(() => {
|
||||
isCalendarReady.value = true;
|
||||
}, []);
|
||||
|
||||
const dayHeaderColor = useMemo(() => {
|
||||
return isSameDate(todaysDate, selectedDate) ? "white" : "#4d4d4d";
|
||||
}, [selectedDate, mode]);
|
||||
const containerStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(isCalendarReady.value ? 1 : 0, {duration: 500}),
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
const dateStyle = useMemo(() => {
|
||||
if (mode === "week") return undefined;
|
||||
return isSameDate(todaysDate, selectedDate) && mode === "day"
|
||||
? styles.dayHeader
|
||||
: styles.otherDayHeader;
|
||||
}, [selectedDate, mode]);
|
||||
const monthStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(mode === 'month' ? 1 : 0, {duration: 300}),
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const memoizedHeaderContentStyle = useMemo(() => {
|
||||
if (mode === "day") {
|
||||
return styles.dayModeHeader;
|
||||
} else if (mode === "week") {
|
||||
return styles.weekModeHeader;
|
||||
} else if (mode === "month") {
|
||||
return styles.monthModeHeader;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}, [mode]);
|
||||
const detailedDayStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(mode === 'day' ? 1 : 0, {duration: 300}),
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const { enrichedEvents, filteredEvents } = useMemo(() => {
|
||||
const startTime = Date.now(); // Start timer
|
||||
|
||||
const startOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
||||
const endOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
||||
|
||||
const filteredEvents =
|
||||
events?.filter(
|
||||
(event) =>
|
||||
event.start &&
|
||||
event.end &&
|
||||
isWithinInterval(event.start, {
|
||||
start: subDays(selectedDate, startOffset),
|
||||
end: addDays(selectedDate, endOffset),
|
||||
}) &&
|
||||
isWithinInterval(event.end, {
|
||||
start: subDays(selectedDate, startOffset),
|
||||
end: addDays(selectedDate, endOffset),
|
||||
})
|
||||
) ?? [];
|
||||
|
||||
const enrichedEvents = filteredEvents.reduce((acc, event) => {
|
||||
const dateKey = event.start.toISOString().split("T")[0];
|
||||
acc[dateKey] = acc[dateKey] || [];
|
||||
acc[dateKey].push({
|
||||
...event,
|
||||
overlapPosition: false,
|
||||
overlapCount: 0,
|
||||
});
|
||||
|
||||
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, CalendarEvent[]>);
|
||||
|
||||
const endTime = Date.now();
|
||||
console.log(
|
||||
"memoizedEvents computation time:",
|
||||
endTime - startTime,
|
||||
"ms"
|
||||
);
|
||||
|
||||
return { enrichedEvents, filteredEvents };
|
||||
}, [events, selectedDate, mode]);
|
||||
|
||||
const renderCustomDateForMonth = (date: Date) => {
|
||||
const circleStyle = useMemo<ViewStyle>(
|
||||
() => ({
|
||||
width: 30,
|
||||
height: 30,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderRadius: 15,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const defaultStyle = useMemo<ViewStyle>(
|
||||
() => ({
|
||||
...circleStyle,
|
||||
}),
|
||||
[circleStyle]
|
||||
);
|
||||
|
||||
const currentDateStyle = useMemo<ViewStyle>(
|
||||
() => ({
|
||||
...circleStyle,
|
||||
backgroundColor: "#4184f2",
|
||||
}),
|
||||
[circleStyle]
|
||||
);
|
||||
|
||||
const renderDate = useCallback(
|
||||
(date: Date) => {
|
||||
const isCurrentDate = isSameDate(todaysDate, date);
|
||||
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
|
||||
|
||||
return (
|
||||
<View style={{ alignItems: "center" }}>
|
||||
<View style={appliedStyle}>
|
||||
<Text style={{ color: isCurrentDate ? "white" : "black" }}>
|
||||
{date.getDate()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[todaysDate, currentDateStyle, defaultStyle] // dependencies
|
||||
);
|
||||
|
||||
return renderDate(date);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOffsetMinutes(getTotalMinutes());
|
||||
}, [events, mode]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// console.log(enrichedEvents, filteredEvents)
|
||||
const detailedMultiStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(mode === (isTablet ? 'week' : '3days') ? 1 : 0, {duration: 300}),
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
bodyContainerStyle={styles.calHeader}
|
||||
swipeEnabled
|
||||
mode={mode}
|
||||
enableEnrichedEvents={true}
|
||||
sortedMonthView
|
||||
// enrichedEventsByDate={enrichedEvents}
|
||||
events={filteredEvents}
|
||||
// eventCellStyle={memoizedEventCellStyle}
|
||||
onPressEvent={handlePressEvent}
|
||||
weekStartsOn={memoizedWeekStartsOn}
|
||||
height={calendarHeight}
|
||||
activeDate={todaysDate}
|
||||
date={selectedDate}
|
||||
onPressCell={handlePressCell}
|
||||
headerContentStyle={memoizedHeaderContentStyle}
|
||||
onSwipeEnd={handleSwipeEnd}
|
||||
scrollOffsetMinutes={offsetMinutes}
|
||||
theme={{
|
||||
palette: {
|
||||
nowIndicator: profileData?.eventColor || "#fd1575",
|
||||
gray: {
|
||||
"100": "#e8eaed",
|
||||
"200": "#e8eaed",
|
||||
"500": "#b7b7b7",
|
||||
"800": "#919191",
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
sm: { fontFamily: "Manrope_600SemiBold", fontSize: 15 },
|
||||
xl: {
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
},
|
||||
moreLabel: {},
|
||||
xs: { fontSize: 10 },
|
||||
},
|
||||
}}
|
||||
dayHeaderStyle={dateStyle}
|
||||
dayHeaderHighlightColor={"white"}
|
||||
showAdjacentMonths
|
||||
hourStyle={styles.hourStyle}
|
||||
onPressDateHeader={handlePressDayHeader}
|
||||
ampm
|
||||
// renderCustomDateForMonth={renderCustomDateForMonth}
|
||||
/>
|
||||
<View style={styles.root}>
|
||||
{(isLoading || isSyncing) && mode !== 'month' && (
|
||||
<Animated.View
|
||||
exiting={FadeOut.duration(300)}
|
||||
style={styles.loadingContainer}
|
||||
>
|
||||
{isSyncing && <Text>Syncing...</Text>}
|
||||
<ActivityIndicator size="large" color="#0000ff"/>
|
||||
</Animated.View>
|
||||
)}
|
||||
<Animated.View style={containerStyle}>
|
||||
<Animated.View style={monthStyle} pointerEvents={mode === 'month' ? 'auto' : 'none'}>
|
||||
<MonthCalendar/>
|
||||
</Animated.View>
|
||||
<Animated.View style={detailedDayStyle} pointerEvents={mode === 'day' ? 'auto' : 'none'}>
|
||||
<DetailedCalendar mode="day" {...props} />
|
||||
</Animated.View>
|
||||
<Animated.View style={detailedMultiStyle}
|
||||
pointerEvents={mode === (isTablet ? 'week' : '3days') ? 'auto' : 'none'}>
|
||||
{!isLoading && (
|
||||
<DetailedCalendar
|
||||
onLoad={handleRenderComplete}
|
||||
mode={isTablet ? 'week' : '3days'}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
segmentslblStyle: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
calHeader: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
dayModeHeader: {
|
||||
alignSelf: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
alignContent: "center",
|
||||
width: 38,
|
||||
right: 42,
|
||||
height: 13,
|
||||
},
|
||||
weekModeHeader: {},
|
||||
monthModeHeader: {},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
dayHeader: {
|
||||
backgroundColor: "#4184f2",
|
||||
aspectRatio: 1,
|
||||
borderRadius: 100,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
otherDayHeader: {
|
||||
backgroundColor: "transparent",
|
||||
color: "#919191",
|
||||
aspectRatio: 1,
|
||||
borderRadius: 100,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
hourStyle: {
|
||||
color: "#5f6368",
|
||||
fontSize: 12,
|
||||
fontFamily: "Manrope_500Medium",
|
||||
},
|
||||
root: {
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 100,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
},
|
||||
});
|
||||
|
||||
export default EventCalendar;
|
||||
124
components/pages/calendar/EventCell.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||
import {Text} from 'react-native-ui-lib';
|
||||
import CachedImage from 'expo-cached-image';
|
||||
import {format} from 'date-fns';
|
||||
import {colorMap, getEventTextColor} from '@/constants/colorMap';
|
||||
|
||||
|
||||
interface EventCellProps {
|
||||
event: any;
|
||||
onPress: (event: any) => void;
|
||||
attendees: any[];
|
||||
}
|
||||
|
||||
export const EventCell: React.FC<EventCellProps> = React.memo((
|
||||
{
|
||||
event,
|
||||
onPress,
|
||||
attendees
|
||||
}) => {
|
||||
const [textColor, setTextColor] = useState<string>(getEventTextColor(event.color)); // Add optional chaining
|
||||
|
||||
useEffect(() => {
|
||||
setTextColor(getEventTextColor(event?.color))
|
||||
}, [event])
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => onPress(event)}
|
||||
style={[styles.eventCell, {backgroundColor: event.eventColor}]}
|
||||
>
|
||||
<Text style={[styles.eventTitle, {color: textColor}]} numberOfLines={1}>
|
||||
{event.title}
|
||||
</Text>
|
||||
<Text style={[styles.eventTitle, {fontSize: 9, opacity: 0.95, color: textColor}]}>
|
||||
{format(new Date(event.start.dateTime), 'h:mm a')} - {format(new Date(event.end.dateTime), 'h:mm a')}
|
||||
</Text>
|
||||
|
||||
{attendees.length > 0 && (
|
||||
<View style={styles.attendeesContainer}>
|
||||
{attendees.slice(0, 3).map((attendee, index) => (
|
||||
<View
|
||||
key={attendee?.uid}
|
||||
style={[styles.attendeeIcon, {left: index * 19}]}
|
||||
>
|
||||
{attendee.pfp ? (
|
||||
<CachedImage
|
||||
source={{uri: attendee.pfp}}
|
||||
style={styles.attendeeImage}
|
||||
cacheKey={attendee.pfp}
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.attendeeInitials}>
|
||||
{attendee?.firstName?.at(0)}
|
||||
{attendee?.lastName?.at(0)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
{attendees.length > 3 && (
|
||||
<View style={[styles.attendeeCount, {left: 3 * 19}]}>
|
||||
<Text style={styles.attendeeCountText}>
|
||||
+{attendees.length - 3}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
eventCell: {
|
||||
flex: 1,
|
||||
borderRadius: 5,
|
||||
paddingHorizontal: 8,
|
||||
paddingBottom: 8,
|
||||
height: '100%',
|
||||
},
|
||||
eventTitle: {
|
||||
fontSize: 12,
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
},
|
||||
attendeesContainer: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 8,
|
||||
height: 27.32,
|
||||
},
|
||||
attendeeIcon: {
|
||||
position: 'absolute',
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 50,
|
||||
borderWidth: 2,
|
||||
borderColor: '#f2f2f2',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
attendeeImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
attendeeInitials: {
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
attendeeCount: {
|
||||
position: 'absolute',
|
||||
width: 27.32,
|
||||
height: 27.32,
|
||||
borderRadius: 50,
|
||||
borderWidth: 2,
|
||||
borderColor: '#f2f2f2',
|
||||
backgroundColor: colorMap.pink,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
attendeeCountText: {
|
||||
color: 'white',
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
@ -1,37 +1,35 @@
|
||||
import {View} from "react-native-ui-lib";
|
||||
import React, {useRef, useState} from "react";
|
||||
import React, {useCallback, useRef, useState} from "react";
|
||||
import {LayoutChangeEvent} from "react-native";
|
||||
import CalendarViewSwitch from "@/components/pages/calendar/CalendarViewSwitch";
|
||||
import {AddEventDialog} from "@/components/pages/calendar/AddEventDialog";
|
||||
import {ManuallyAddEventModal} from "@/components/pages/calendar/ManuallyAddEventModal";
|
||||
import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader";
|
||||
import {EventCalendar} from "@/components/pages/calendar/EventCalendar";
|
||||
|
||||
export const InnerCalendar = () => {
|
||||
const [calendarHeight, setCalendarHeight] = useState(0);
|
||||
const [calendarWidth, setCalendarWidth] = useState(0);
|
||||
const calendarContainerRef = useRef(null);
|
||||
const hasSetInitialSize = useRef(false);
|
||||
|
||||
const onLayout = (event: LayoutChangeEvent) => {
|
||||
const {height, width} = event.nativeEvent.layout;
|
||||
setCalendarHeight(height);
|
||||
setCalendarWidth(width);
|
||||
};
|
||||
const onLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
if (!hasSetInitialSize.current) {
|
||||
const width = event.nativeEvent.layout.width;
|
||||
setCalendarWidth(width);
|
||||
hasSetInitialSize.current = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={{flex: 1, backgroundColor: "#fff", borderRadius: 30, marginBottom: 60, overflow: "hidden"}}
|
||||
style={{flex: 1, backgroundColor: "#fff", borderRadius: 0, marginBottom: 0, overflow: "hidden"}}
|
||||
ref={calendarContainerRef}
|
||||
onLayout={onLayout}
|
||||
paddingB-0
|
||||
>
|
||||
<CalendarHeader/>
|
||||
{calendarHeight > 0 && (
|
||||
<EventCalendar
|
||||
calendarHeight={calendarHeight}
|
||||
calendarWidth={calendarWidth}
|
||||
/>
|
||||
)}
|
||||
<EventCalendar
|
||||
calendarWidth={calendarWidth}
|
||||
/>
|
||||
</View>
|
||||
<CalendarViewSwitch/>
|
||||
|
||||
|
||||
572
components/pages/calendar/MonthCalendar.tsx
Normal file
@ -0,0 +1,572 @@
|
||||
import React, {useCallback, useMemo, useRef} from 'react';
|
||||
import {Dimensions, StyleSheet, Text, TouchableOpacity, View,} from 'react-native';
|
||||
import {
|
||||
addDays,
|
||||
addMonths,
|
||||
eachDayOfInterval,
|
||||
endOfMonth,
|
||||
format,
|
||||
isSameDay,
|
||||
isSameMonth,
|
||||
isWithinInterval,
|
||||
startOfMonth,
|
||||
} from 'date-fns';
|
||||
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
||||
import {useSetAtom} from "jotai";
|
||||
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {FlashList} from "@shopify/flash-list";
|
||||
import * as Device from "expo-device";
|
||||
import {CalendarController} from "@/components/pages/calendar/CalendarController";
|
||||
|
||||
interface CalendarEvent {
|
||||
id: string;
|
||||
title: string;
|
||||
start: Date;
|
||||
end: Date;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface CustomMonthCalendarProps {
|
||||
weekStartsOn?: 0 | 1;
|
||||
}
|
||||
|
||||
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const MAX_VISIBLE_EVENTS = 3;
|
||||
const CENTER_MONTH_INDEX = 12;
|
||||
|
||||
|
||||
const Event = React.memo(({event, onPress}: { event: CalendarEvent; onPress: () => void }) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.event, {backgroundColor: event?.eventColor || '#6200ee'}]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={styles.eventText} numberOfLines={1}>
|
||||
{event.title}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
));
|
||||
|
||||
interface CalendarEvent {
|
||||
id: string;
|
||||
title: string;
|
||||
start: Date;
|
||||
end: Date;
|
||||
color?: string;
|
||||
weekPosition?: number;
|
||||
}
|
||||
|
||||
const MultiDayEvent = React.memo(({
|
||||
event,
|
||||
isStart,
|
||||
isEnd,
|
||||
onPress,
|
||||
}: {
|
||||
event: CalendarEvent;
|
||||
dayWidth: number;
|
||||
isStart: boolean;
|
||||
isEnd: boolean;
|
||||
onPress: () => void;
|
||||
}) => {
|
||||
const style = {
|
||||
position: 'absolute' as const,
|
||||
height: 14,
|
||||
backgroundColor: event?.eventColor || '#6200ee',
|
||||
padding: 2,
|
||||
zIndex: 1,
|
||||
left: isStart ? 4 : -0.5, // Extend slightly into the border
|
||||
right: isEnd ? 4 : -0.5, // Extend slightly into the border
|
||||
top: event.weekPosition ? event.weekPosition * 24 : 0,
|
||||
borderRadius: 4,
|
||||
borderTopLeftRadius: isStart ? 4 : 0,
|
||||
borderBottomLeftRadius: isStart ? 4 : 0,
|
||||
borderTopRightRadius: isEnd ? 4 : 0,
|
||||
borderBottomRightRadius: isEnd ? 4 : 0,
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={style} onPress={onPress}>
|
||||
{isStart && (
|
||||
<Text style={[styles.eventText]} numberOfLines={1}>
|
||||
{event.title}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
const Day = React.memo((
|
||||
{
|
||||
date,
|
||||
events,
|
||||
multiDayEvents,
|
||||
dayWidth,
|
||||
onPress
|
||||
}: {
|
||||
date: Date;
|
||||
events: CalendarEvent[];
|
||||
multiDayEvents: CalendarEvent[];
|
||||
dayWidth: number;
|
||||
onPress: (date: Date) => void;
|
||||
}) => {
|
||||
const isCurrentMonth = isSameMonth(date, new Date());
|
||||
const isToday = isSameDay(date, new Date());
|
||||
|
||||
const remainingSlots = Math.max(0, MAX_VISIBLE_EVENTS - multiDayEvents.length);
|
||||
const singleDayEvents = events.filter(event => !event.isMultiDay);
|
||||
const visibleSingleDayEvents = singleDayEvents.slice(0, remainingSlots);
|
||||
const totalHiddenEvents = singleDayEvents.length - remainingSlots;
|
||||
|
||||
const maxMultiDayPosition = multiDayEvents.length > 0
|
||||
? Math.max(...multiDayEvents.map(e => e.weekPosition || 0)) + 1
|
||||
: 0;
|
||||
const multiDayEventsHeight = maxMultiDayPosition * 16; // Height for multi-day events
|
||||
|
||||
return (
|
||||
<View style={[styles.day, {width: dayWidth}]}>
|
||||
<TouchableOpacity
|
||||
style={styles.dayContent}
|
||||
onPress={() => onPress(date)}
|
||||
>
|
||||
<View style={[
|
||||
styles.dateContainer,
|
||||
isToday && {backgroundColor: events?.[0]?.eventColor},
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.dateText,
|
||||
!isCurrentMonth && styles.outsideMonthText,
|
||||
isToday && styles.todayText,
|
||||
]}>
|
||||
{format(date, 'd')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Multi-day events container */}
|
||||
<View style={[styles.multiDayContainer, {height: multiDayEventsHeight}]}>
|
||||
{multiDayEvents.map(event => (
|
||||
<MultiDayEvent
|
||||
key={event.id}
|
||||
event={event}
|
||||
dayWidth={dayWidth}
|
||||
isStart={isSameDay(date, event.start)}
|
||||
isEnd={isSameDay(date, event.end)}
|
||||
onPress={() => onPress(event.start)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Single-day events container */}
|
||||
<View style={[styles.singleDayContainer, {marginTop: multiDayEventsHeight}]}>
|
||||
{visibleSingleDayEvents.map(event => (
|
||||
<Event
|
||||
key={event.id}
|
||||
event={event}
|
||||
onPress={() => onPress(event.start)}
|
||||
/>
|
||||
))}
|
||||
{totalHiddenEvents > 0 && (
|
||||
<Text style={styles.moreEvents}>
|
||||
{totalHiddenEvents} More
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
const {data: rawEvents} = useGetEvents();
|
||||
const setSelectedDate = useSetAtom(selectedDateAtom);
|
||||
const setMode = useSetAtom(modeAtom);
|
||||
const {profileData} = useAuthContext();
|
||||
|
||||
const scrollViewRef = useRef<FlashList<any>>(null);
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
const screenWidth = isTablet ? Dimensions.get('window').width * 0.89 : Dimensions.get('window').width;
|
||||
const screenHeight = isTablet ? Dimensions.get('window').height * 0.89 : Dimensions.get('window').height;
|
||||
const dayWidth = screenWidth / 7;
|
||||
const centerMonth = useRef(new Date());
|
||||
|
||||
const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1;
|
||||
|
||||
const onDayPress = useCallback(
|
||||
(date: Date) => {
|
||||
date && setSelectedDate(date);
|
||||
setTimeout(() => {
|
||||
setMode("day");
|
||||
}, 100)
|
||||
},
|
||||
[setSelectedDate, setMode]
|
||||
);
|
||||
|
||||
const getMonthData = useCallback((date: Date) => {
|
||||
const start = startOfMonth(date);
|
||||
const end = endOfMonth(date);
|
||||
const days = eachDayOfInterval({start, end});
|
||||
|
||||
const firstDay = days[0];
|
||||
const startPadding = [];
|
||||
let startDay = firstDay.getDay();
|
||||
while (startDay !== weekStartsOn) {
|
||||
startDay = (startDay - 1 + 7) % 7;
|
||||
startPadding.unshift(addDays(firstDay, -startPadding.length - 1));
|
||||
}
|
||||
|
||||
const lastDay = days[days.length - 1];
|
||||
const endPadding = [];
|
||||
let endDay = lastDay.getDay();
|
||||
while (endDay !== (weekStartsOn + 6) % 7) {
|
||||
endDay = (endDay + 1) % 7;
|
||||
endPadding.push(addDays(lastDay, endPadding.length + 1));
|
||||
}
|
||||
|
||||
return [...startPadding, ...days, ...endPadding];
|
||||
}, [weekStartsOn]);
|
||||
|
||||
const monthsToRender = useMemo(() => {
|
||||
const months = [];
|
||||
for (let i = -CENTER_MONTH_INDEX; i <= CENTER_MONTH_INDEX; i++) {
|
||||
const monthDate = addMonths(centerMonth.current, i);
|
||||
months.push({
|
||||
date: monthDate,
|
||||
days: getMonthData(monthDate)
|
||||
});
|
||||
}
|
||||
return months;
|
||||
}, [getMonthData, rawEvents]);
|
||||
|
||||
const processedEvents = useMemo(() => {
|
||||
if (!rawEvents?.length) return {
|
||||
eventMap: new Map(),
|
||||
multiDayEvents: []
|
||||
};
|
||||
|
||||
const eventMap = new Map();
|
||||
const multiDayEvents: CalendarEvent[] = [];
|
||||
|
||||
rawEvents.forEach((event) => {
|
||||
if (!event?.start || !event?.end) return;
|
||||
|
||||
const startDate = event.start instanceof Date ? event.start : new Date(event.start);
|
||||
const endDate = event.end instanceof Date ? event.end : new Date(event.end);
|
||||
|
||||
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) return;
|
||||
|
||||
const duration = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (duration > 1) {
|
||||
multiDayEvents.push({
|
||||
...event,
|
||||
isMultiDay: true,
|
||||
start: startDate,
|
||||
end: endDate
|
||||
});
|
||||
} else {
|
||||
const dateStr = format(startDate, 'yyyy-MM-dd');
|
||||
const existing = eventMap.get(dateStr) || [];
|
||||
eventMap.set(dateStr, [...existing, {...event, start: startDate, end: endDate}]);
|
||||
}
|
||||
});
|
||||
|
||||
multiDayEvents.sort((a, b) => {
|
||||
if (!a.start || !b.start || !a.end || !b.end) return 0;
|
||||
const durationA = a.end.getTime() - a.start.getTime();
|
||||
const durationB = b.end.getTime() - b.start.getTime();
|
||||
return durationB - durationA;
|
||||
});
|
||||
|
||||
return {eventMap, multiDayEvents};
|
||||
}, [rawEvents]);
|
||||
|
||||
const getMultiDayEventsForDay = useCallback((date: Date) => {
|
||||
return processedEvents.multiDayEvents.filter(event => {
|
||||
if (!event.start || !event.end) return false;
|
||||
return isWithinInterval(date, {
|
||||
start: event.start,
|
||||
end: event.end
|
||||
});
|
||||
});
|
||||
}, [processedEvents.multiDayEvents]);
|
||||
|
||||
const getEventsForDay = useCallback((date: Date) => {
|
||||
const dateStr = format(date, 'yyyy-MM-dd');
|
||||
return processedEvents.eventMap.get(dateStr) || [];
|
||||
}, [processedEvents.eventMap]);
|
||||
|
||||
const sortedDaysOfWeek = useMemo(() => {
|
||||
const days = [...DAYS_OF_WEEK];
|
||||
return days.slice(weekStartsOn).concat(days.slice(0, weekStartsOn));
|
||||
}, [weekStartsOn]);
|
||||
|
||||
const renderMonth = useCallback(({item}: { item: MonthData }) => (
|
||||
<Month
|
||||
date={item.date}
|
||||
days={item.days}
|
||||
getEventsForDay={getEventsForDay}
|
||||
getMultiDayEventsForDay={getMultiDayEventsForDay}
|
||||
dayWidth={dayWidth}
|
||||
onPress={onDayPress}
|
||||
screenWidth={screenWidth}
|
||||
sortedDaysOfWeek={sortedDaysOfWeek}
|
||||
/>
|
||||
), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<CalendarController
|
||||
scrollViewRef={scrollViewRef}
|
||||
centerMonthIndex={CENTER_MONTH_INDEX}
|
||||
/>
|
||||
<FlashList
|
||||
ref={scrollViewRef}
|
||||
data={monthsToRender}
|
||||
renderItem={renderMonth}
|
||||
keyExtractor={keyExtractor}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
initialScrollIndex={CENTER_MONTH_INDEX}
|
||||
removeClippedSubviews={true}
|
||||
estimatedItemSize={screenWidth}
|
||||
estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}}
|
||||
maintainVisibleContentPosition={{
|
||||
minIndexForVisible: 0,
|
||||
autoscrollToTopThreshold: 10,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
type MonthData = {
|
||||
date: Date;
|
||||
days: Date[];
|
||||
};
|
||||
|
||||
const keyExtractor = (item: MonthData, index: number) => `month-${index}`;
|
||||
|
||||
const Month = React.memo(({
|
||||
date,
|
||||
days,
|
||||
getEventsForDay,
|
||||
getMultiDayEventsForDay,
|
||||
dayWidth,
|
||||
onPress,
|
||||
screenWidth,
|
||||
sortedDaysOfWeek
|
||||
}: {
|
||||
date: Date;
|
||||
days: Date[];
|
||||
getEventsForDay: (date: Date) => CalendarEvent[];
|
||||
getMultiDayEventsForDay: (date: Date) => CalendarEvent[];
|
||||
dayWidth: number;
|
||||
onPress: (date: Date) => void;
|
||||
screenWidth: number;
|
||||
sortedDaysOfWeek: string[];
|
||||
}) => {
|
||||
const weeks = useMemo(() => {
|
||||
const result = [];
|
||||
for (let i = 0; i < days.length; i += 7) {
|
||||
result.push(days.slice(i, i + 7));
|
||||
}
|
||||
return result;
|
||||
}, [days]);
|
||||
|
||||
const eventPositions = useMemo(() => {
|
||||
const positions = new Map<string, number>();
|
||||
const weekTracking = new Map<number, Set<string>>();
|
||||
|
||||
weeks.forEach((week, weekIndex) => {
|
||||
const activeEvents = new Set<string>();
|
||||
|
||||
week.forEach(day => {
|
||||
const events = getMultiDayEventsForDay(day);
|
||||
events.forEach(event => {
|
||||
activeEvents.add(event.id);
|
||||
});
|
||||
});
|
||||
|
||||
weekTracking.set(weekIndex, activeEvents);
|
||||
|
||||
activeEvents.forEach(eventId => {
|
||||
if (!positions.has(eventId)) {
|
||||
const prevWeekEvents = weekIndex > 0 ? weekTracking.get(weekIndex - 1) : new Set<string>();
|
||||
const usedPositions = new Set<number>();
|
||||
|
||||
if (prevWeekEvents) {
|
||||
prevWeekEvents.forEach(prevEventId => {
|
||||
if (activeEvents.has(prevEventId)) {
|
||||
usedPositions.add(positions.get(prevEventId) || 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let position = 0;
|
||||
while (usedPositions.has(position)) {
|
||||
position++;
|
||||
}
|
||||
positions.set(eventId, position);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return positions;
|
||||
}, [weeks, getMultiDayEventsForDay]);
|
||||
|
||||
return (
|
||||
<View style={[styles.scrollView, {width: screenWidth}]}>
|
||||
<View style={styles.monthHeader}>
|
||||
<Text style={styles.monthText}>{format(date, 'MMMM yyyy')}</Text>
|
||||
<View style={styles.weekDayRow}>
|
||||
{sortedDaysOfWeek.map((day, index) => (
|
||||
<Text key={index} style={styles.weekDayText}>{day}</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.daysGrid}>
|
||||
{weeks.map((week, weekIndex) => (
|
||||
<React.Fragment key={weekIndex}>
|
||||
{week.map((date, dayIndex) => {
|
||||
const multiDayEvents = getMultiDayEventsForDay(date).map(event => ({
|
||||
...event,
|
||||
weekPosition: eventPositions.get(event.id) || 0
|
||||
}));
|
||||
|
||||
return (
|
||||
<Day
|
||||
key={`${weekIndex}-${dayIndex}`}
|
||||
date={date}
|
||||
events={getEventsForDay(date)}
|
||||
multiDayEvents={multiDayEvents}
|
||||
dayWidth={dayWidth}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const HEADER_HEIGHT = 40;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
multiDayContainer: {
|
||||
position: 'absolute',
|
||||
top: 29,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 1,
|
||||
},
|
||||
dayContent: {
|
||||
flex: 1,
|
||||
padding: 4, // Move padding here instead
|
||||
},
|
||||
eventsContainer: {
|
||||
flex: 1,
|
||||
marginTop: 2,
|
||||
position: 'relative',
|
||||
},
|
||||
event: {
|
||||
borderRadius: 4,
|
||||
padding: 1,
|
||||
marginVertical: 1,
|
||||
height: 14,
|
||||
},
|
||||
eventText: {
|
||||
fontSize: 10,
|
||||
color: '#fff',
|
||||
fontWeight: '500',
|
||||
},
|
||||
day: {
|
||||
height: '14%',
|
||||
padding: 0,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#eee',
|
||||
position: 'relative',
|
||||
overflow: 'visible',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
monthContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
daysGrid: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
// justifyContent: 'center'
|
||||
},
|
||||
weekDay: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: HEADER_HEIGHT,
|
||||
},
|
||||
scrollContent: {
|
||||
flex: 1,
|
||||
},
|
||||
weekDayText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
},
|
||||
dateContainer: {
|
||||
minWidth: 20,
|
||||
height: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 12,
|
||||
},
|
||||
todayContainer: {
|
||||
backgroundColor: '#6200ee',
|
||||
},
|
||||
dateText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#333',
|
||||
},
|
||||
todayText: {
|
||||
color: '#fff',
|
||||
},
|
||||
outsideMonthText: {
|
||||
color: '#ccc',
|
||||
},
|
||||
moreEvents: {
|
||||
fontSize: 10,
|
||||
color: '#666',
|
||||
textAlign: 'center',
|
||||
},
|
||||
monthHeader: {
|
||||
paddingVertical: 12,
|
||||
},
|
||||
monthText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
textAlign: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
weekDayRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export default MonthCalendar;
|
||||
@ -1,13 +1,27 @@
|
||||
import { atom } from "jotai";
|
||||
import * as Device from "expo-device";
|
||||
import { CalendarEvent } from "@/components/pages/calendar/interfaces";
|
||||
|
||||
const getDefaultMode = () => {
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
return isTablet ? "week" : "3days";
|
||||
};
|
||||
|
||||
export const editVisibleAtom = atom<boolean>(false);
|
||||
export const isAllDayAtom = atom<boolean>(false);
|
||||
export const eventForEditAtom = atom<CalendarEvent | undefined>(undefined);
|
||||
export const isFamilyViewAtom = atom<boolean>(false);
|
||||
export const modeAtom = atom<"week" | "month" | "day">("week");
|
||||
export const modeAtom = atom<"week" | "month" | "day" | "3days">(getDefaultMode());
|
||||
export const selectedDateAtom = atom<Date>(new Date());
|
||||
export const selectedNewEventDateAtom = atom<Date | undefined>(undefined);
|
||||
export const settingsPageIndex = atom<number>(0);
|
||||
export const userSettingsView = atom<boolean>(true);
|
||||
export const toDosPageIndex = atom<number>(0);
|
||||
export const refreshTriggerAtom = atom<boolean>(false);
|
||||
export const refreshEnabledAtom = atom<boolean>(true);
|
||||
export const selectedUserAtom = atom<{
|
||||
uid: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
eventColor?: string;
|
||||
} | null>(null);
|
||||
@ -1,7 +1,8 @@
|
||||
export const modeMap = new Map([
|
||||
[0, "day"],
|
||||
[1, "week"],
|
||||
[2, "month"],
|
||||
[1, "3days"],
|
||||
[2, "week"],
|
||||
[3, "month"]
|
||||
]);
|
||||
|
||||
export const months = [
|
||||
|
||||