61 Commits

Author SHA1 Message Date
5c6ec9eda4 removed the selected date on scroll 2024-10-17 21:04:07 +02:00
0deac561cc build nr update 2024-10-16 22:22:05 +02:00
a5231e8547 build nr update 2024-10-16 21:11:18 +02:00
d00c89ec6c Merge branch 'dev' 2024-10-16 21:07:26 +02:00
e4eb67fba7 ui changes 2024-10-16 19:07:57 +02:00
939b525a4b Dev sync 2024-10-16 04:10:49 +02:00
23f03eca2a Merge branch 'dev' 2024-10-16 04:09:07 +02:00
7effcfe288 merge resolve 2024-10-15 23:09:49 +02:00
43d0d67044 new fonts, ui tweaks 2024-10-15 23:07:21 +02:00
e2d18cf7d5 - Sorted the todo list and showed the todos by sorted date 2024-10-15 22:52:53 +02:00
dc05090f49 - Implemented editing of the todos
- Modified the AddChoreDialog.tsx to be able to edit or add new items
2024-10-15 22:31:53 +02:00
0f75be8d1e Fixed issue with groceries update 2024-10-15 20:34:18 +02:00
f662852c68 update main 2024-10-15 12:02:22 +02:00
656622fd93 Merge branch 'dev' 2024-10-15 11:40:40 +02:00
4b01e18ac0 ui tweaks 2024-10-14 23:02:18 +02:00
c6db12a9b6 changed font 2024-10-14 20:29:00 +02:00
7aa7143d09 use __DEV__ firebase 2024-10-14 20:22:37 +02:00
7d16da78d0 Update build number 2024-10-13 23:52:06 +02:00
a8717a9a42 Cally, not kaly privacy policy 2024-10-13 17:44:51 +02:00
55adf88e6a Cally, not kaly privacy policy 2024-10-13 17:43:58 +02:00
a2ff00df70 Merge pull request #2 from urosran/matija_changes
moved the day on single day mode to the left corner
2024-10-13 17:00:27 +02:00
b37000ec4d Formatting 2024-10-13 17:00:17 +02:00
6962bfc727 moved the day on single day mode to the left corner 2024-10-13 16:50:02 +02:00
341a1bf249 Merge branch 'main' into dev 2024-10-13 15:06:33 +02:00
0f12296ac6 fix all day events 2024-10-13 15:03:53 +02:00
515b5738bb Implementation of fetching, adding and updating todos to db 2024-10-13 13:34:31 +02:00
de854ecfd6 Invalidate events when updating profile 2024-10-13 12:38:00 +02:00
36329dc2f2 ui changes 2024-10-13 11:37:44 +02:00
f4be82587c update icon 2024-10-13 10:41:33 +02:00
1e506dd246 Added creating family devices, refetch calendar on notification received 2024-10-13 10:21:38 +02:00
9123aab566 Merge branch 'main' into dev 2024-10-13 02:52:33 +02:00
ed0c654a55 Seconds & time logic 2024-10-12 20:44:29 +02:00
621c7f1f50 chore points 2024-10-12 17:59:43 +02:00
5c6233a5fd Use auto increment 2024-10-12 12:44:11 +02:00
d97387767b Prebuild 2024-10-12 12:40:56 +02:00
4c021b67f6 package lock updte 2024-10-12 12:36:26 +02:00
28ad8f8986 prebuild 2024-10-12 10:28:36 +02:00
fd08cd84fc Merge branch 'refs/heads/dev'
# Conflicts:
#	app.json
#	ios/cally/Info.plist
2024-10-12 10:26:30 +02:00
b3e40ad909 Use cally everywhere 2024-10-12 10:25:36 +02:00
6e1e665b93 NOtification update 2024-10-12 09:15:30 +02:00
3d4795c25d New build 2024-10-12 07:01:56 +02:00
41ee6d0f3e Merge branch 'dev' of https://github.com/urosran/cally into dev 2024-10-12 00:08:21 +02:00
a8ab69b69f new icons 2024-10-12 00:08:15 +02:00
04a6103470 Notifications 2024-10-11 23:53:08 +02:00
9c6cc16f16 todos collapse 2024-10-11 23:30:08 +02:00
cd62837198 Shopping List backend implementation
- Added creatorId to the grocery item in db and showed the name of the user that requested to add the new grocery
2024-10-11 18:02:13 +02:00
86231daba4 Shopping List backend implementation
- Small code improvements
2024-10-11 16:28:17 +02:00
22e962a8b2 Shopping List backend implementation
- Fixed issue with groceries not fetching after update
2024-10-11 16:20:37 +02:00
e6758a014f Removed leftover logging 2024-10-11 16:12:00 +02:00
9b94aa8e70 Shopping List backend implementation
- Implemented fetching, create and update of groceries in db
2024-10-11 16:11:46 +02:00
a05de1b333 Editing calendar events
- Fixed an issue with the profile data not being read from db becaue of the local state not being updated instantly
2024-10-11 16:08:39 +02:00
562da49806 Editing calendar events
- Added new hook for updating events
- Updated the selected event in db
- Added document id as id to the event object when fetching events
- Removed unused react-native-google sign-in library
2024-10-11 13:24:25 +02:00
7d9d41acfc - Added proper oauth2 authentication with google and saving of the access token to the profile 2024-10-11 10:28:49 +02:00
454d2256db Prebuild v2 2024-10-11 06:43:19 +02:00
f20a7dbdd8 Prebuild 2024-10-11 02:54:03 +02:00
e1c03c840d add braindump 2024-10-11 02:00:11 +02:00
ca0b55c494 Colors, fix family view, updated version 2024-10-10 02:00:14 +02:00
2c099740a2 Merge branch 'main' into dev
# Conflicts:
#	app/(auth)/_layout.tsx
2024-10-09 17:34:15 +02:00
1a5283be47 Trigger build 2024-10-08 21:58:33 +02:00
fb3859e69b Merge branch 'dev' of https://github.com/urosran/cally into dev 2024-10-07 00:11:34 +02:00
2b55c5db0c grocery changes 2024-10-07 00:10:51 +02:00
123 changed files with 5321 additions and 2363 deletions

View File

@ -91,6 +91,9 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
manifestPlaceholders = [
appAuthRedirectScheme: "callyplanner"
]
}
signingConfigs {
debug {

View File

@ -14,9 +14,14 @@
</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">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<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"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://u.expo.dev/bdb8c57b-25bb-4d36-b3b8-5b09c5092f52"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -3,4 +3,5 @@
<color name="iconBackground">#ffffff</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#ffffff</color>
<color name="notification_icon_color">#ffffff</color>
</resources>

View File

@ -1,6 +1,7 @@
<resources>
<string name="app_name">Kali - Family Planner</string>
<string name="app_name">Cally - Family Planner</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>
<string name="expo_runtime_version">1.0.0</string>
</resources>

View File

@ -1,4 +1,4 @@
rootProject.name = 'Kali - Family Planner'
rootProject.name = 'Cally - Family Planner'
dependencyResolutionManagement {
versionCatalogs {

View File

@ -1,6 +1,6 @@
{
"expo": {
"name": "Kali - Family Planner",
"name": "Cally - Family Planner",
"slug": "cally",
"version": "1.0.0",
"orientation": "portrait",
@ -16,7 +16,7 @@
"supportsTablet": true,
"bundleIdentifier": "com.cally.app",
"googleServicesFile": "./ios/GoogleService-Info.plist",
"buildNumber": "17"
"buildNumber": "28"
},
"android": {
"adaptiveIcon": {
@ -24,7 +24,11 @@
"backgroundColor": "#ffffff"
},
"package": "com.cally.app",
"googleServicesFile": "./android/app/google-services.json"
"googleServicesFile": "./android/app/google-services.json",
"permissions": [
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO"
]
},
"web": {
"bundler": "metro",
@ -51,7 +55,15 @@
"microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone",
"recordAudioAndroid": true
}
]
],
[
"expo-notifications",
{
"color": "#ffffff",
"defaultChannel": "default"
}
],
"expo-font"
],
"experiments": {
"typedRoutes": true
@ -63,6 +75,10 @@
"eas": {
"projectId": "bdb8c57b-25bb-4d36-b3b8-5b09c5092f52"
}
},
"runtimeVersion": "1.0.0",
"updates": {
"url": "https://u.expo.dev/bdb8c57b-25bb-4d36-b3b8-5b09c5092f52"
}
}
}

View File

@ -16,6 +16,13 @@ import {
MaterialCommunityIcons,
Octicons,
} from "@expo/vector-icons";
import MenuIcon from "@/assets/svgs/MenuIcon";
import { router } from "expo-router";
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";
export default function TabLayout() {
const { mutateAsync: signOut } = useSignOut();
@ -30,13 +37,13 @@ export default function TabLayout() {
backgroundColor: "#f9f8f7",
height: "100%",
},
drawerIcon: () => "OVDE"
drawerIcon: () => <MenuIcon />,
}}
drawerContent={(props) => {
return (
<DrawerContentScrollView {...props} style={{ height: "100%" }}>
<View centerH centerV margin-30>
<Text text50>Welcome to Kali</Text>
<Text style={styles.title}>Welcome to Cally</Text>
</View>
<View
style={{
@ -50,26 +57,14 @@ export default function TabLayout() {
color="rgb(7, 184, 199)"
bgColor={"rgb(231, 248, 250)"}
pressFunc={() => props.navigation.navigate("calendar")}
icon={
<Feather
name="calendar"
size={30}
color="rgb(7, 184, 199)"
/>
}
icon={<NavCalendarIcon />}
/>
<DrawerButton
color="#50be0c"
title={"Groceries"}
bgColor={"#eef9e7"}
pressFunc={() => props.navigation.navigate("grocery")}
icon={
<MaterialCommunityIcons
name="food-apple-outline"
size={30}
color="#50be0c"
/>
}
icon={<NavGroceryIcon />}
/>
</View>
<View style={{ flex: 1 }}>
@ -88,19 +83,17 @@ export default function TabLayout() {
/>*/}
<DrawerButton
color="#8005eb"
title={"To Dos"}
title={"To Do's"}
bgColor={"#f3e6fd"}
pressFunc={() => props.navigation.navigate("todos")}
icon={
<AntDesign name="checkcircleo" size={30} color="#8005eb" />
}
icon={<NavToDosIcon />}
/>
<DrawerButton
color="#e0ca03"
title={"Brain Dump"}
bgColor={"#fffacb"}
pressFunc={() => props.navigation.navigate("brain_dump")}
icon={<AntDesign name="book" size={30} color="#e0ca03" />}
icon={<NavBrainDumpIcon />}
/>
{/*<DrawerItem label="Logout" onPress={() => signOut()} />*/}
</View>
@ -108,6 +101,7 @@ export default function TabLayout() {
<Button
onPress={() => props.navigation.navigate("settings")}
label={"Manage Settings"}
labelStyle={styles.label}
iconSource={() => (
<View
backgroundColor="#ededed"
@ -118,7 +112,7 @@ export default function TabLayout() {
centerV
centerH
>
<Octicons name="gear" size={30} color="#6c645b" />
<NavSettingsIcon />
</View>
)}
backgroundColor="white"
@ -126,8 +120,8 @@ export default function TabLayout() {
paddingV-30
marginH-30
marginB-10
borderRadius={15}
style={{ elevation: 1 }}
borderRadius={18.55}
style={{ elevation: 0 }}
/>
<Button
@ -135,13 +129,14 @@ export default function TabLayout() {
marginH-30
paddingV-15
style={{
marginTop: "47%",
marginTop: "42%",
backgroundColor: "transparent",
borderWidth: 2,
borderWidth: 1.3,
borderColor: "#fd1775",
}}
label="Sign out of Kali"
label="Sign out of Cally"
color="#fd1775"
labelStyle={styles.signOut}
onPress={() => signOut()}
/>
</DrawerContentScrollView>
@ -195,9 +190,19 @@ export default function TabLayout() {
name="todos"
options={{
drawerLabel: "To-Do",
title: "To-Do",
title: "To-Do's",
}}
/>
</Drawer>
);
}
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"
}
});

View File

@ -1,58 +1,275 @@
import {DarkTheme, DefaultTheme, ThemeProvider} from '@react-navigation/native';
import {useFonts} from 'expo-font';
import {Stack} from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import {useEffect} from 'react';
import 'react-native-reanimated';
import {useColorScheme} from '@/hooks/useColorScheme';
import {AuthContextProvider} from "@/contexts/AuthContext";
import React, { useEffect } from "react";
import { DefaultTheme, ThemeProvider } from "@react-navigation/native";
import {
useFonts,
Manrope_200ExtraLight,
Manrope_300Light,
Manrope_400Regular,
Manrope_500Medium,
Manrope_600SemiBold,
Manrope_700Bold,
Manrope_800ExtraBold,
} from "@expo-google-fonts/manrope";
import {
PlusJakartaSans_200ExtraLight,
PlusJakartaSans_300Light,
PlusJakartaSans_400Regular,
PlusJakartaSans_500Medium,
PlusJakartaSans_600SemiBold,
PlusJakartaSans_700Bold,
PlusJakartaSans_800ExtraBold,
PlusJakartaSans_200ExtraLight_Italic,
PlusJakartaSans_300Light_Italic,
PlusJakartaSans_400Regular_Italic,
PlusJakartaSans_500Medium_Italic,
PlusJakartaSans_600SemiBold_Italic,
PlusJakartaSans_700Bold_Italic,
PlusJakartaSans_800ExtraBold_Italic,
} from "@expo-google-fonts/plus-jakarta-sans";
import {
Poppins_100Thin,
Poppins_100Thin_Italic,
Poppins_200ExtraLight,
Poppins_200ExtraLight_Italic,
Poppins_300Light,
Poppins_300Light_Italic,
Poppins_400Regular,
Poppins_400Regular_Italic,
Poppins_500Medium,
Poppins_500Medium_Italic,
Poppins_600SemiBold,
Poppins_600SemiBold_Italic,
Poppins_700Bold,
Poppins_700Bold_Italic,
Poppins_800ExtraBold,
Poppins_800ExtraBold_Italic,
Poppins_900Black,
Poppins_900Black_Italic,
} from "@expo-google-fonts/poppins";
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 {
ThemeManager,
Typography,
Toast,
TextProps,
} from "react-native-ui-lib";
import functions from "@react-native-firebase/functions";
import firestore from "@react-native-firebase/firestore";
import auth from "@react-native-firebase/auth";
import {QueryClient, QueryClientProvider} from "react-query";
import {Toast} from "react-native-ui-lib";
import firestore from "@react-native-firebase/firestore";
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
const queryClient = new QueryClient()
const queryClient = new QueryClient();
if (__DEV__) {
functions().useEmulator("localhost", 5001);
firestore().useEmulator("localhost", 5471);
auth().useEmulator("http://localhost:9099");
}
// if (__DEV__) {
// functions().useEmulator('localhost', 5001);
// firestore().useEmulator("localhost", 5471);
// auth().useEmulator("http://localhost:9099");
// }
type TextStyleBase =
| "text10"
| "text20"
| "text30"
| "text40"
| "text50"
| "text60"
| "text70"
| "text80"
| "text90"
| "text100";
type TextStyleModifier = "R" | "M" | "BO" | "H" | "BL" | "L";
type TextStyle = TextStyleBase | `${TextStyleBase}${TextStyleModifier}`;
type TextStyleProps = {
[K in TextStyle]?: boolean;
};
type ExtendedTextProps = TextProps & TextStyleProps;
interface FontStyle {
fontFamily: string;
fontSize: number;
}
const getManropeFontStyle = (style: TextStyle): FontStyle => {
let fontFamily: string;
let fontSize: number;
if (style.includes("L") || style.includes("BL"))
fontFamily = "Manrope_300Light";
else if (style.includes("R")) fontFamily = "Manrope_400Regular";
else if (style.includes("M")) fontFamily = "Manrope_500Medium";
else if (style.includes("BO") || style.includes("H"))
fontFamily = "Manrope_700Bold";
else {
const baseStyle = style.slice(0, 6) as TextStyleBase;
switch (baseStyle) {
case "text10":
case "text20":
fontFamily = "Manrope_700Bold";
break;
case "text30":
case "text40":
fontFamily = "Manrope_600SemiBold";
break;
case "text50":
fontFamily = "Manrope_400Regular";
break;
default:
fontFamily = "Manrope_300Light";
}
}
switch (style.slice(0, 6) as TextStyleBase) {
case "text10":
fontSize = 64;
break;
case "text20":
fontSize = 50;
break;
case "text30":
fontSize = 36;
break;
case "text40":
fontSize = 28;
break;
case "text50":
fontSize = 24;
break;
case "text60":
fontSize = 20;
break;
case "text70":
fontSize = 16;
break;
case "text80":
fontSize = 14;
break;
case "text90":
fontSize = 12;
break;
case "text100":
fontSize = 10;
break;
default:
fontSize = 16;
}
return { fontFamily, fontSize };
};
export default function RootLayout() {
const [loaded] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
});
const [loaded] = useFonts({
Manrope_200ExtraLight,
Manrope_300Light,
Manrope_400Regular,
Manrope_500Medium,
Manrope_600SemiBold,
Manrope_700Bold,
Manrope_800ExtraBold,
PlusJakartaSans_200ExtraLight,
PlusJakartaSans_300Light,
PlusJakartaSans_400Regular,
PlusJakartaSans_500Medium,
PlusJakartaSans_600SemiBold,
PlusJakartaSans_700Bold,
PlusJakartaSans_800ExtraBold,
PlusJakartaSans_200ExtraLight_Italic,
PlusJakartaSans_300Light_Italic,
PlusJakartaSans_400Regular_Italic,
PlusJakartaSans_500Medium_Italic,
PlusJakartaSans_600SemiBold_Italic,
PlusJakartaSans_700Bold_Italic,
PlusJakartaSans_800ExtraBold_Italic,
Poppins_100Thin,
Poppins_100Thin_Italic,
Poppins_200ExtraLight,
Poppins_200ExtraLight_Italic,
Poppins_300Light,
Poppins_300Light_Italic,
Poppins_400Regular,
Poppins_400Regular_Italic,
Poppins_500Medium,
Poppins_500Medium_Italic,
Poppins_600SemiBold,
Poppins_600SemiBold_Italic,
Poppins_700Bold,
Poppins_700Bold_Italic,
Poppins_800ExtraBold,
Poppins_800ExtraBold_Italic,
Poppins_900Black,
Poppins_900Black_Italic,
});
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
const typographies: Partial<Record<TextStyle, FontStyle>> = {};
(
[
"text10",
"text20",
"text30",
"text40",
"text50",
"text60",
"text70",
"text80",
"text90",
"text100",
] as const
).forEach((baseStyle) => {
typographies[baseStyle] = getManropeFontStyle(baseStyle);
(["R", "M", "BO", "H", "BL", "L"] as const).forEach((modifier) => {
const style = `${baseStyle}${modifier}` as TextStyle;
typographies[style] = getManropeFontStyle(style);
});
});
Typography.loadTypographies(typographies);
ThemeManager.setComponentTheme(
"Text",
(props: ExtendedTextProps, context: unknown) => {
const textStyle = (
Object.keys(props) as Array<keyof ExtendedTextProps>
).find((key) => typographies[key as TextStyle]) as
| TextStyle
| undefined;
return {
style: [
Typography.text50,
textStyle ? typographies[textStyle] : undefined,
],
};
}
}, [loaded]);
if (!loaded) {
return null;
);
}
}, [loaded]);
return (
<QueryClientProvider client={queryClient}>
<AuthContextProvider>
<ThemeProvider value={DefaultTheme}>
<Stack>
<Stack.Screen name="(auth)" options={{headerShown: false}}/>
<Stack.Screen name="(unauth)" options={{headerShown: false}}/>
<Stack.Screen name="+not-found"/>
</Stack>
<Toast />
</ThemeProvider>
</AuthContextProvider>
</QueryClientProvider>
);
if (!loaded) {
return null;
}
return (
<QueryClientProvider client={queryClient}>
<AuthContextProvider>
<ThemeProvider value={DefaultTheme}>
<Stack>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(unauth)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<Toast />
</ThemeProvider>
</AuthContextProvider>
</QueryClientProvider>
);
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,20 @@
import * as React from "react"
import Svg, { Path, SvgProps } from "react-native-svg"
const AddImageIcon: React.FC<SvgProps> = (props) => (
<Svg
width={props.width || 28}
height={props.width || 28}
viewBox="0 0 28 28"
fill="none"
{...props}
>
<Path
stroke={props.color || "#FD1775"}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.936}
d="M16.833 20.433 14.34 17.96c-1.146-1.137-1.719-1.705-2.379-1.918a2.903 2.903 0 0 0-1.785 0c-.66.213-1.233.781-2.379 1.918L1.99 23.808m14.843-3.375.496-.492c1.17-1.16 1.755-1.74 2.426-1.952a2.903 2.903 0 0 1 1.811.02c.666.228 1.238.821 2.381 2.008l1.214 1.232m-8.328-.816 5.743 5.844M1.99 23.807c.044.379.121.676.255.939.279.546.723.99 1.27 1.269.62.316 1.434.316 3.06.316h13.94c.901 0 1.553 0 2.061-.054M1.99 23.807c-.061-.515-.061-1.183-.061-2.122V7.745c0-1.626 0-2.439.316-3.06.279-.547.723-.99 1.27-1.27.62-.316 1.434-.316 3.06-.316h5.518m10.483 23.178c.408-.043.723-.121 1-.262.546-.279.99-.723 1.268-1.27.317-.62.317-1.434.317-3.06v-5.518m-2.904-5.808V6.003m0 0V1.647m0 4.356h4.356m-4.356 0H17.9"
/>
</Svg>
)
export default AddImageIcon

View File

@ -0,0 +1,19 @@
import * as React from "react"
import Svg, { SvgProps, Path } from "react-native-svg"
const AddPersonIcon = (props: SvgProps) => (
<Svg
width={props. width || 23}
height={props. width || 23}
viewBox="0 0 23 23"
fill="none"
{...props}
>
<Path
fill={props.color || "#8A8A8A"}
fillRule="evenodd"
d="M22.089 19.5a1 1 0 0 1-1 .998h-1v1.05a1 1 0 0 1-2 0v-1.05h-1a1 1 0 0 1-1-.998 1 1 0 0 1 1-.998h1v-.945a1 1 0 0 1 2 0v.945h1a1 1 0 0 1 1 .998Zm-11.377-9.045a7.844 7.844 0 0 0-.654-.033c-.203 0-.402.014-.601.03a2.997 2.997 0 0 1-2.368-2.924 3 3 0 0 1 6 0 2.999 2.999 0 0 1-2.377 2.927Zm-8.605 7.05c-.014.005-.004 0-.018.006V3.53c0-.55.448-.99 1-.99h6.847a4.994 4.994 0 0 0-4.847 4.984c0 1.441.622 2.73 1.602 3.642-2.478 1.147-4.27 3.516-4.584 6.338ZM14.992 6.52a4.955 4.955 0 0 0-4.837-3.98h6.934a1 1 0 0 1 1 .997v8.979a1 1 0 0 0 2 0V2.54a1.997 1.997 0 0 0-2-1.995h-16c-1.105 0-2 .893-2 1.995v15.962c0 1.102.895 1.996 2 1.996h10a1 1 0 0 0 1-.998 1 1 0 0 0-1-.998H4.064s-.006-.065-.006-.106c0-6.084 7.386-6.888 9.24-5.168 1.074.995 2.465-.59 1.387-1.354a7.933 7.933 0 0 0-1.221-.712c1.214-1.115 1.889-2.785 1.528-4.641Z"
clipRule="evenodd"
/>
</Svg>
)
export default AddPersonIcon

21
assets/svgs/BinIcon.tsx Normal file
View File

@ -0,0 +1,21 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface BinIconProps extends React.SVGProps<SVGSVGElement> {}
const BinIcon: React.FC<BinIconProps> = (props) => (
<Svg
width={19}
height={21}
fill="none"
{...props}
>
<Path
stroke="#B7B7B7"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.7}
d="m15.568 4.122-.834 12.5c-.072 1.095-.11 1.642-.345 2.057-.209.366-.523.66-.901.843-.43.208-.979.208-2.076.208H7.237c-1.097 0-1.646 0-2.076-.208a2.081 2.081 0 0 1-.9-.843c-.237-.415-.274-.962-.347-2.057l-.833-12.5M1 4.122h16.649m-4.162 0-.282-.845c-.273-.819-.41-1.228-.662-1.53a2.08 2.08 0 0 0-.835-.603C11.34 1 10.909 1 10.046 1H8.603c-.863 0-1.295 0-1.662.144a2.081 2.081 0 0 0-.835.602c-.253.303-.39.712-.662 1.53l-.282.846m6.244 4.162v7.284M7.243 8.284v7.284"
/>
</Svg>
)
export default BinIcon

View File

@ -0,0 +1,19 @@
import * as React from "react"
import Svg, { Path, SvgProps } from "react-native-svg"
const CalendarIcon: React.FC<SvgProps> = (props) => (
<Svg
width={props.width || 21}
height={props.height || 21}
fill="none"
{...props}
>
<Path
stroke={props.color || "#FD1775"}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.455}
d="M1.413 7.359h18.294m-4.065 4.067-10.164-.002m3.388 4.066H5.478m0-14.23v2.033m10.164-2.032v2.032M4.665 19.555h11.79c1.138 0 1.708 0 2.142-.222.383-.194.694-.505.889-.888.221-.435.221-1.004.221-2.142V6.546c0-1.139 0-1.708-.221-2.143a2.032 2.032 0 0 0-.889-.888c-.434-.222-1.004-.222-2.142-.222H4.665c-1.138 0-1.707 0-2.142.222a2.033 2.033 0 0 0-.888.888c-.222.435-.222 1.004-.222 2.143v9.757c0 1.138 0 1.707.222 2.142.195.383.505.694.888.888.435.222 1.004.222 2.142.222Z"
/>
</Svg>
)
export default CalendarIcon

View File

@ -0,0 +1,21 @@
import * as React from "react"
import Svg, { G, Path, Defs, ClipPath, SvgProps } from "react-native-svg"
const FireworksOrangeIcon: React.FC<SvgProps> = (props) => (
<Svg
width={props.width || 29}
height={props.height || 28}
fill="none"
{...props}
>
<G fill={props.color || "#F90"} clipPath="url(#a)">
<Path d="M14.573 4.229c.527 0 .885-1.15 1.022-2.31C15.765.468 15.152 0 14.573 0c-.58 0-1.193.467-1.023 1.92.137 1.16.495 2.309 1.023 2.309ZM14.573 22.987c-.528 0-.886 1.15-1.023 2.31-.17 1.452.443 1.92 1.023 1.92.58 0 1.193-.468 1.022-1.92-.137-1.16-.496-2.31-1.022-2.31ZM9.85 5.473c.456-.262.192-1.438-.27-2.51-.578-1.343-1.343-1.44-1.845-1.151-.503.29-.799 1.001.074 2.174.7.935 1.583 1.751 2.04 1.487ZM19.296 21.743c-.456.261-.193 1.438.268 2.51.58 1.343 1.343 1.44 1.846 1.15.502-.289.8-1.001-.074-2.173-.698-.935-1.583-1.751-2.04-1.487ZM4.95 6.845c-1.172-.873-1.884-.576-2.174-.074-.29.503-.192 1.267 1.15 1.846 1.073.461 2.249.724 2.512.269.263-.457-.554-1.343-1.489-2.041ZM25.217 18.6c-1.07-.463-2.247-.724-2.51-.27-.263.458.554 1.343 1.489 2.042 1.172.872 1.883.575 2.172.073.29-.502.193-1.266-1.15-1.845ZM5.193 13.607c0-.526-1.15-.884-2.309-1.021-1.452-.171-1.918.443-1.918 1.021 0 .581.466 1.194 1.918 1.024 1.159-.137 2.31-.496 2.31-1.024ZM26.261 12.585c-1.158.138-2.308.496-2.308 1.023 0 .527 1.15.886 2.308 1.022 1.453.172 1.92-.442 1.92-1.022 0-.58-.467-1.193-1.92-1.023ZM3.927 18.6c-1.343.579-1.44 1.343-1.15 1.845.29.502 1 .8 2.172-.073.935-.699 1.751-1.584 1.488-2.041-.262-.455-1.438-.194-2.51.269ZM25.217 8.617c1.344-.58 1.442-1.343 1.15-1.846-.288-.502-.999-.799-2.171.074-.935.698-1.752 1.584-1.489 2.04.263.456 1.44.194 2.51-.268ZM7.81 23.23c-.874 1.172-.578 1.884-.075 2.174.503.29 1.267.192 1.845-1.15.462-1.073.726-2.25.27-2.511-.457-.264-1.342.552-2.04 1.487ZM21.336 3.986c.874-1.173.578-1.884.074-2.174-.502-.29-1.267-.192-1.846 1.151-.46 1.072-.724 2.248-.268 2.51.456.264 1.342-.552 2.04-1.487ZM13.464 9.362c.398-.106.435-1.04.304-1.938-.164-1.125-.718-1.353-1.153-1.235-.436.114-.803.589-.383 1.645.337.844.838 1.636 1.232 1.528ZM15.682 17.852c-.394.107-.434 1.044-.303 1.941.164 1.125.718 1.353 1.154 1.234.435-.116.802-.59.382-1.644-.336-.844-.837-1.636-1.233-1.53ZM9.14 8.194c-.316.32-.398.91.496 1.617.71.56 1.542.996 1.829.707.29-.29-.144-1.12-.704-1.831-.705-.893-1.3-.811-1.62-.493ZM20.002 19.023c.319-.319.4-.912-.492-1.618-.712-.56-1.54-.994-1.83-.706-.289.29.145 1.12.707 1.831.704.892 1.297.811 1.615.493ZM8.38 12.79c.898.13 1.834.093 1.938-.304.107-.396-.685-.897-1.528-1.233-1.055-.42-1.527-.052-1.645.383-.117.434.11.99 1.234 1.154ZM20.767 14.427c-.898-.13-1.832-.092-1.94.303-.106.396.686.896 1.529 1.232 1.056.42 1.53.053 1.646-.382.118-.436-.11-.99-1.235-1.153ZM8.8 15.95c.842-.336 1.635-.838 1.528-1.235-.105-.393-1.042-.432-1.94-.3-1.124.162-1.352.717-1.234 1.151.116.436.59.803 1.646.385ZM20.346 11.266c-.843.338-1.634.838-1.528 1.233.106.396 1.041.434 1.94.304 1.125-.164 1.352-.72 1.235-1.153-.117-.437-.59-.804-1.647-.384ZM9.649 17.422c-.89.703-.81 1.3-.49 1.617.319.317.912.399 1.616-.493.56-.71.996-1.54.707-1.83-.291-.29-1.12.144-1.833.706ZM19.496 9.796c.892-.705.81-1.3.494-1.617-.319-.32-.914-.4-1.617.493-.563.712-.997 1.542-.707 1.83.289.29 1.118-.146 1.83-.706ZM13.45 17.862c-.394-.106-.896.685-1.232 1.529-.42 1.056-.055 1.53.383 1.645.435.118.99-.11 1.153-1.234.131-.898.091-1.834-.304-1.94ZM16.545 6.18c-.434-.119-.99.11-1.152 1.234-.131.899-.093 1.833.302 1.94.395.107.897-.686 1.233-1.528.42-1.056.055-1.529-.383-1.647ZM12.37 13.607a2.204 2.204 0 1 0 4.407.001 2.204 2.204 0 0 0-4.408 0Z" />
</G>
<Defs>
<ClipPath id="a">
<Path fill={props.color || "#fff"} d="M.965 0H28.18v27.216H.965z" />
</ClipPath>
</Defs>
</Svg>
)
export default FireworksOrangeIcon

23
assets/svgs/GiftIcon.tsx Normal file
View File

@ -0,0 +1,23 @@
import * as React from "react";
import Svg, { Path, SvgProps } from "react-native-svg";
const GiftIcon: React.FC<SvgProps> = (props) => (
<Svg width={props.width || 14} height={props.height || 14} fill="none" {...props}>
<Path
fill={props.color || "#46A80A"}
d="M6.09 12.581V8.834H1.872v-.937H6.09V4.149H1.873v8.432H6.09Zm.936 0h4.216V4.15H7.026v3.748h4.216v.937H7.026v3.747ZM.936 3.212H12.18v9.838a.469.469 0 0 1-.468.468H1.405a.468.468 0 0 1-.468-.468V3.212Z"
/>
<Path
fill={props.color || "#46A80A"}
d="M.468 3.212h12.18c.312 0 .468.157.468.469 0 .312-.156.468-.468.468H.468C.156 4.15 0 3.993 0 3.681c0-.312.156-.469.468-.469Z"
/>
<Path
fill={props.color || "#46A80A"}
d="M5.153 3.213a.937.937 0 1 0 0-1.874.937.937 0 0 0 0 1.874Zm0 .936a1.874 1.874 0 1 1 0-3.747 1.874 1.874 0 0 1 0 3.747Z"
/>
<Path
fill={props.color || "#46A80A"}
d="M7.964 3.213a.937.937 0 1 0 0-1.874.937.937 0 0 0 0 1.874Zm0 .936a1.874 1.874 0 1 1 0-3.747 1.874 1.874 0 0 1 0 3.747Z"
/>
</Svg>
);
export default GiftIcon;

View File

@ -9,6 +9,7 @@ const MenuIcon: React.FC<MenuIconProps> = (props) => (
<Svg
width={24}
height={16}
viewBox="0 0 24 16"
fill="none"
{...props}
>

View File

@ -0,0 +1,36 @@
import * as React from "react"
import Svg, { Path, SvgProps } from "react-native-svg"
const NavBrainDumpIcon: React.FC<SvgProps> = (props) => (
<Svg
width={22}
height={28}
fill="none"
{...props}
>
<Path
stroke="#F90"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
strokeWidth={1.509}
d="M21.1.859H3.994C2.284.859.976 2.167.976 3.877c0 1.71 1.308 3.019 3.018 3.019H21.1V27.02"
/>
<Path
stroke="#F90"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
strokeWidth={1.509}
d="M21.1 27.021H3.994c-1.71 0-3.018-1.308-3.018-3.018V3.878M21.097 3.878H3.991"
/>
<Path
stroke="#F90"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
strokeWidth={1.509}
d="M6.007 6.897v12.075l3.019-1.006 3.018 1.006V6.897"
/>
</Svg>
)
export default NavBrainDumpIcon

View File

@ -0,0 +1,20 @@
import * as React from "react"
import Svg, { Path, SvgProps } from "react-native-svg"
const NavCalendarIcon: React.FC<SvgProps> = (props) => (
<Svg
width={28}
height={28}
viewBox="0 0 28 28"
fill="none"
{...props}
>
<Path
stroke={props.color || "#07B8C7"}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.124}
d="M1.825 10.075h25.043m-5.565 5.567L7.39 15.64m4.638 5.566H7.39m0-19.478V4.51m13.913-2.782V4.51M6.277 26.77h16.139c1.558 0 2.337 0 2.933-.303.523-.267.949-.692 1.216-1.216.303-.595.303-1.375.303-2.933V8.962c0-1.558 0-2.337-.303-2.933a2.782 2.782 0 0 0-1.216-1.216c-.596-.303-1.375-.303-2.933-.303H6.277c-1.558 0-2.337 0-2.933.303-.523.267-.949.693-1.216 1.216-.303.596-.303 1.375-.303 2.933v13.356c0 1.558 0 2.338.303 2.933.267.523.693.95 1.216 1.216.596.303 1.375.303 2.933.303Z"
/>
</Svg>
)
export default NavCalendarIcon

View File

@ -0,0 +1,20 @@
import * as React from "react";
import Svg, { Path, SvgProps } from "react-native-svg";
const NavGroceryIcon: React.FC<SvgProps> = (props) => (
<Svg
width={29}
height={32}
fill="none"
{...props}
>
<Path
fill="#50BE0C"
d="M27.8 12c-.888-2.27-2.662-4.14-4.983-4.956-2.518-.883-5.143-.382-7.636.307-.047-1.362.068-2.728.262-4.076.11-.761.634-2.335-.668-2.389-1.081-.045-1.084 1.402-1.19 2.14-.203 1.416-.323 2.85-.284 4.281-.599-.1-1.19-.322-1.787-.446a13.485 13.485 0 0 0-2.704-.3c-1.752 0-3.48.465-4.922 1.479C1.09 10.01 0 13.446.046 16.747c.052 3.575 1.263 6.99 3.3 9.905.899 1.288 1.932 2.522 3.21 3.451 1.241.903 2.628 1.325 4.133 1.54.64.093 1.29.096 1.927-.009.557-.09 1.078-.325 1.63-.419.417-.07 1.005.258 1.408.348a6.297 6.297 0 0 0 2.096.107c2.563-.292 4.594-1.449 6.28-3.387 3.71-4.264 5.921-10.791 3.77-16.284Zm-1.69 8.743c-.474 1.663-1.225 3.182-2.195 4.608-.86 1.263-1.855 2.514-3.131 3.377-1.172.79-2.76 1.223-4.173 1.093-.719-.065-1.373-.41-2.087-.478-.8-.076-1.538.352-2.318.459-3.064.416-5.485-1.665-7.146-3.972-2.04-2.831-3.249-6.214-3.134-9.732.106-3.223 1.643-6.55 4.998-7.432 1.678-.44 3.446-.188 5.099.238.715.183 1.454.48 2.196.52.572.066 1.2-.196 1.74-.344 3.233-.89 6.873-1.105 9.124 1.855 2.08 2.732 1.925 6.666 1.027 9.808Z"
/>
<Path
fill="#50BE0C"
d="M12 17.966c-.778.285-1.977.71-2.432 1.454.11-.183.221-.363-.002.002-.222.365-.113.186-.001.003-.668 1.099.272 2.53 1.547 2.347 1.258-.18 1.378-1.419 1.364-2.443a8.822 8.822 0 0 0-.095-1.282c-.043-.264-.157-.164-.382-.08ZM18.096 18.917c-.435-.304-.919-.542-1.405-.752-.118-.05-.698-.375-.832-.297.018-.01.036-.021 0 0-.036.023-.02.012-.001.001-.11.07-.096.609-.104.716a9.003 9.003 0 0 0-.01 1.534c.1 1.117 1.049 2.082 2.228 1.517 1.165-.558 1.095-2.04.124-2.719ZM12.223 3.815C12.343.28 8.347-.271 6.922.432c.371 4.075 3.48 4.164 5.301 3.383Z"
/>
</Svg>
);
export default NavGroceryIcon;

View File

@ -0,0 +1,28 @@
import * as React from "react";
import Svg, { Path, SvgProps } from "react-native-svg";
const NavSettingsIcon: React.FC<SvgProps> = (props) => (
<Svg
width={26}
height={28}
fill="none"
{...props}
>
<Path
stroke="#6C645B"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
strokeWidth={1.69}
d="M12.972 17.893a3.887 3.887 0 1 0 0-7.774 3.887 3.887 0 0 0 0 7.774Z"
/>
<Path
stroke="#6C645B"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
strokeWidth={1.69}
d="m24.398 8.531-.972-1.683a1.943 1.943 0 0 0-2.655-.711l-.511.295c-1.944 1.121-4.373-.28-4.373-2.525v-.59c0-1.074-.87-1.944-1.944-1.944H12c-1.073 0-1.943.87-1.943 1.943v.591c0 2.244-2.43 3.647-4.373 2.525l-.511-.295a1.943 1.943 0 0 0-2.655.711l-.972 1.683a1.943 1.943 0 0 0 .711 2.655l.511.296c1.944 1.122 1.944 3.927 0 5.049l-.51.295a1.943 1.943 0 0 0-.712 2.655l.972 1.684a1.943 1.943 0 0 0 2.655.71l.51-.294c1.944-1.123 4.374.28 4.374 2.524v.591c0 1.074.87 1.944 1.944 1.944h1.943c1.074 0 1.944-.87 1.944-1.944v-.59c0-2.245 2.43-3.648 4.373-2.526l.511.296c.93.536 2.119.219 2.655-.711l.972-1.684a1.943 1.943 0 0 0-.712-2.655l-.51-.295c-1.944-1.122-1.944-3.927 0-5.05l.51-.295a1.942 1.942 0 0 0 .712-2.655Z"
/>
</Svg>
);
export default NavSettingsIcon;

View File

@ -0,0 +1,19 @@
import * as React from "react"
import Svg, { Path, SvgProps } from "react-native-svg"
const NavToDosIcon: React.FC<SvgProps> = (props) => (
<Svg
width={props.width || 30}
height={props.width || 30}
viewBox="0 0 30 30"
fill="none"
{...props}
>
<Path
fill={props.color || "#8005EB"}
stroke={props.color || "#8005EB"}
strokeWidth={0.5}
d="M15.038 29.649c-7.953 0-14.4-6.447-14.4-14.4s6.447-14.4 14.4-14.4 14.4 6.447 14.4 14.4-6.447 14.4-14.4 14.4Zm0-1.44c7.157 0 12.96-5.802 12.96-12.96 0-7.158-5.803-12.96-12.96-12.96-7.158 0-12.96 5.802-12.96 12.96 0 7.158 5.802 12.96 12.96 12.96Zm4.53-17.07a.72.72 0 1 1 1.019 1.019l-7.2 7.2a.72.72 0 0 1-1.018 0l-2.88-2.88a.72.72 0 1 1 1.018-1.018l2.37 2.37 6.692-6.69Z"
/>
</Svg>
)
export default NavToDosIcon

20
assets/svgs/PenIcon.tsx Normal file
View File

@ -0,0 +1,20 @@
import * as React from "react";
import Svg, { Path } from "react-native-svg";
interface PenIconProps extends React.SVGProps<SVGSVGElement> {}
const PenIcon: React.FC<PenIconProps> = (props) => (
<Svg
xmlns="http://www.w3.org/2000/svg"
width={17}
height={19}
fill="none"
{...props}
>
<Path
fill="#919191"
d="M9.46 4.489a.907.907 0 1 0-1.416-1.135l1.417 1.135Zm-7.844 8.337.687.592a.835.835 0 0 0 .021-.025l-.708-.567Zm-.207.505-.906-.059v.017l.906.042Zm-.17 3.658-.906-.042c-.002.045 0 .09.004.135l.903-.093Zm.953.831.03.907c.06-.002.12-.01.18-.024l-.21-.883Zm3.63-.86.21.883.013-.003-.223-.88Zm.466-.295.7.578.008-.01-.708-.568Zm8.016-8.546a.908.908 0 0 0-1.416-1.136l1.416 1.136ZM8.048 3.353A.908.908 0 0 0 9.464 4.49L8.048 3.353Zm2.63-1.828.707.568a.942.942 0 0 0 .035-.046l-.742-.522Zm1.97-.387.581-.697a.915.915 0 0 0-.09-.067l-.49.764Zm2.707 2.253.643-.64a.941.941 0 0 0-.062-.057l-.58.697Zm-.01 1.982-.636-.648a.92.92 0 0 0-.072.08l.707.568Zm-2.457 1.61a.907.907 0 1 0 1.416 1.136l-1.416-1.136ZM9.654 3.787a.907.907 0 1 0-1.795.269l1.795-.27Zm4.064 4.663a.908.908 0 0 0-.244-1.798l.244 1.798ZM8.044 3.354.908 12.258l1.416 1.135L9.461 4.49 8.044 3.354ZM.93 12.233c-.251.291-.4.656-.426 1.04l1.812.117c-.001.01-.005.02-.012.028L.93 12.233Zm-.427 1.056-.169 3.658 1.813.084.17-3.658-1.814-.084Zm-.165 3.793a1.834 1.834 0 0 0 1.884 1.645l-.059-1.814h-.007a.02.02 0 0 1-.006-.004.02.02 0 0 1-.005-.006.018.018 0 0 1-.002-.007l-1.805.186ZM2.4 18.703l3.63-.86-.419-1.766-3.63.86.42 1.766Zm3.644-.863c.37-.094.7-.303.943-.598l-1.4-1.155a.021.021 0 0 1 .01-.006l.447 1.759Zm.95-.607 7.309-9.114-1.416-1.136-7.308 9.114 1.416 1.136ZM9.465 4.489l1.921-2.396L9.97.958 8.048 3.353 9.464 4.49Zm1.956-2.442a.544.544 0 0 1 .739-.145l.98-1.528a2.36 2.36 0 0 0-3.204.63l1.485 1.043Zm.648-.211 2.707 2.253 1.161-1.395L13.229.44l-1.16 1.395Zm2.645 2.196a.487.487 0 0 1 .143.347l1.815.01a2.303 2.303 0 0 0-.673-1.639l-1.285 1.282Zm.143.347c-.001.13-.054.255-.147.346l1.27 1.296c.44-.43.688-1.017.692-1.632l-1.816-.01Zm-.22.426-1.748 2.178 1.416 1.136 1.748-2.178-1.415-1.136Zm-6.777-.75a5.215 5.215 0 0 0 5.86 4.395l-.245-1.798a3.4 3.4 0 0 1-3.82-2.865l-1.795.269Z"
/>
</Svg>
);
export default PenIcon;

View File

@ -0,0 +1,23 @@
import * as React from "react"
import Svg, { Path, SvgProps } from "react-native-svg"
const PrivacyPolicyIcon: React.FC<SvgProps> = (props) => (
<Svg
width={props.width ||20}
height={props.height ||22}
fill="none"
{...props}
>
<Path
stroke={props.color || "#6C645B"}
strokeWidth={1.2}
d="M1.413 8.656c0-3.7 0-5.55 1.15-6.7 1.15-1.15 3-1.15 6.7-1.15h1.962c3.7 0 5.55 0 6.7 1.15 1.149 1.15 1.149 3 1.149 6.7v3.925c0 3.7 0 5.55-1.15 6.7-1.15 1.149-3 1.149-6.7 1.149H9.263c-3.7 0-5.55 0-6.7-1.15-1.149-1.15-1.149-3-1.149-6.7V8.657Z"
/>
<Path
stroke={props.color || "#6C645B"}
strokeLinecap="round"
strokeWidth={1.2}
d="M6.324 10.617h7.85M6.324 6.694h7.85M6.324 14.541h4.906"
/>
</Svg>
)
export default PrivacyPolicyIcon

View File

@ -0,0 +1,27 @@
import * as React from "react"
import Svg, { Path, SvgProps } from "react-native-svg"
const ProfileIcon: React.FC<SvgProps> = (props) => (
<Svg
width={22}
height={22}
viewBox="0 0 21 21"
fill="none"
{...props}
>
<Path
stroke={props.color || "red"}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.414}
d="M10.956 11.538a.91.91 0 0 0-.226 0 3.083 3.083 0 0 1-2.98-3.084 3.089 3.089 0 0 1 3.093-3.093 3.09 3.09 0 0 1 .113 6.176ZM17.199 17.762a9.368 9.368 0 0 1-6.356 2.47 9.368 9.368 0 0 1-6.356-2.47c.095-.886.66-1.754 1.67-2.433 2.583-1.716 6.808-1.716 9.373 0 1.009.68 1.575 1.547 1.669 2.433Z"
/>
<Path
stroke={props.color || "red"}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.414}
d="M10.843 20.233a9.43 9.43 0 1 0 0-18.86 9.43 9.43 0 0 0 0 18.86Z"
/>
</Svg>
)
export default ProfileIcon

View File

@ -0,0 +1,21 @@
import * as React from "react";
import Svg, { SvgProps, Path } from "react-native-svg";
const RemindersIcon = (props: SvgProps) => (
<Svg
width={props.width || 22}
height={props.width || 22}
viewBox="0 0 22 22"
fill="none"
{...props}
>
<Path
fill={props.color || "#919191"}
fillRule="evenodd"
stroke={props.color || "#919191"}
strokeWidth={0.4}
d="M3.974 3.96C7.964-.023 14.45.02 18.464 4.034c4.016 4.016 4.057 10.506.067 14.495-3.99 3.99-10.48 3.95-14.495-.067a10.315 10.315 0 0 1-2.94-8.705.788.788 0 1 1 1.561.214 8.738 8.738 0 0 0 2.494 7.376c3.41 3.411 8.902 3.43 12.265.067C20.78 14.05 20.76 8.56 17.35 5.148c-3.41-3.41-8.897-3.43-12.26-.072l.786.004a.788.788 0 1 1-.008 1.576L3.19 6.643a.788.788 0 0 1-.785-.784l-.013-2.677a.788.788 0 1 1 1.577-.007l.004.786Zm7.276 2.293c.435 0 .788.353.788.789v3.878l2.398 2.398a.788.788 0 1 1-1.115 1.115l-2.86-2.86V7.043c0-.436.354-.789.789-.789Z"
clipRule="evenodd"
/>
</Svg>
);
export default RemindersIcon;

View File

@ -1,4 +1,5 @@
export async function fetchGoogleCalendarEvents(token, startDate, endDate) {
console.log(token);
const response = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/primary/events?single_events=true&time_min=${startDate}&time_max=${endDate}`,
{
@ -9,6 +10,7 @@ export async function fetchGoogleCalendarEvents(token, startDate, endDate) {
);
const data = await response.json();
console.log(data);
const googleEvents = [];
data.items?.forEach((item) => {
let isAllDay = false;

View File

@ -0,0 +1,101 @@
import { View, Text, Button, TextField } from "react-native-ui-lib";
import React, { useEffect, useState } from "react";
import { Dialog } from "react-native-ui-lib";
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
import CloseXIcon from "@/assets/svgs/CloseXIcon";
import { Dimensions, StyleSheet } from "react-native";
import DropModalIcon from "@/assets/svgs/DropModalIcon";
import MenuIcon from "@/assets/svgs/MenuIcon";
import { useBrainDumpContext } from "@/contexts/DumpContext";
interface IAddBrainDumpProps {
isVisible: boolean;
setIsVisible: (value: boolean) => void;
}
const AddBrainDump = ({
addBrainDumpProps,
}: {
addBrainDumpProps: IAddBrainDumpProps;
}) => {
const { addBrainDump } = useBrainDumpContext();
const [dumpTitle, setDumpTitle] = useState<string>("");
const [dumpDesc, setDumpDesc] = useState<string>("");
const { width, height } = Dimensions.get("screen");
useEffect(() => {
setDumpDesc("");
setDumpTitle("");
}, [addBrainDumpProps.isVisible]);
return (
<Dialog
bottom={true}
height={"90%"}
width={width}
panDirection={PanningDirectionsEnum.DOWN}
onDismiss={() => addBrainDumpProps.setIsVisible(false)}
containerStyle={{
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
backgroundColor: "white",
padding: 0,
paddingTop: 3,
margin: 0,
}}
visible={addBrainDumpProps.isVisible}
>
<View row spread style={styles.topBtns} marginB-20>
<Button
color="#05a8b6"
label="Cancel"
style={styles.topBtn}
onPress={() => {
addBrainDumpProps.setIsVisible(false);
}}
/>
<DropModalIcon
style={{ marginTop: 15 }}
onPress={() => addBrainDumpProps.setIsVisible(false)}
/>
<Button
color="#05a8b6"
label="Save"
style={styles.topBtn}
onPress={() => {
addBrainDump({ id: 99, title: dumpTitle, description: dumpDesc });
addBrainDumpProps.setIsVisible(false);
}}
/>
</View>
<View marginH-20>
<TextField
value={dumpTitle}
placeholder="Set Title"
text60R
onChangeText={(text) => {
setDumpTitle(text);
}}
/>
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20 />
<TextField
value={dumpDesc}
placeholder="Write Description"
text70
onChangeText={(text) => {
setDumpDesc(text);
}}
/>
</View>
</Dialog>
);
};
const styles = StyleSheet.create({
topBtns: {},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
});
export default AddBrainDump;

View File

@ -1,59 +1,120 @@
import { ScrollView } from "react-native";
import { Dimensions, ScrollView } from "react-native";
import React, { useState } from "react";
import { View, Text } from "react-native-ui-lib";
import { View, Text, Button } from "react-native-ui-lib";
import DumpList from "./DumpList";
import HeaderTemplate from "@/components/shared/HeaderTemplate";
import { TextField } from "react-native-ui-lib";
import { StyleSheet } from "react-native";
import { Feather } from "@expo/vector-icons";
import { Feather, MaterialIcons } from "@expo/vector-icons";
import { TextInput } from "react-native-gesture-handler";
import AddBrainDump from "./AddBrainDump";
import LinearGradient from "react-native-linear-gradient";
const BrainDumpPage = () => {
const [searchText, setSearchText] = useState<string>("");
const [isAddVisible, setIsAddVisible] = useState<boolean>(false);
return (
<View>
<ScrollView
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
>
<View marginH-25>
<HeaderTemplate
message={"Welcome to your notes!"}
isWelcome={false}
/>
<View>
<View style={styles.searchField} centerV>
<TextField
value={searchText}
onChangeText={(value) => {
setSearchText(value);
}}
leadingAccessory={
<Feather
name="search"
size={24}
color="#9b9b9b"
style={{ paddingRight: 10 }}
/>
}
placeholder="Search notes..."
/>
<View height={"100%"}>
<View>
<ScrollView
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
>
<View marginH-25>
<HeaderTemplate
message={"Welcome to your notes!"}
isWelcome={false}
children={
<Text
style={{ fontFamily: "Manrope_400Regular", fontSize: 14 }}
>
Drop your notes on-the-go here, and{"\n"}organize them later.
</Text>
}
/>
<View>
<View style={styles.searchField} centerV>
<TextField
value={searchText}
onChangeText={(value) => {
setSearchText(value);
}}
leadingAccessory={
<Feather
name="search"
size={24}
color="#9b9b9b"
style={{ paddingRight: 10 }}
/>
}
style={{
fontFamily: "Manrope_500Medium",
fontSize: 15,
}}
placeholder="Search notes..."
/>
</View>
<DumpList searchText={searchText} />
</View>
<DumpList searchText={searchText} />
</View>
</View>
</ScrollView>
</ScrollView>
</View>
<LinearGradient
colors={["#f2f2f2", "transparent"]}
start={{ x: 0.5, y: 1 }}
end={{ x: 0.5, y: 0 }}
style={{
position: "absolute",
bottom: 0,
height: 90,
width: Dimensions.get("screen").width,
}}
>
<Button
style={{
height: 40,
position: "relative",
marginLeft: "auto",
width: 20,
right: 20,
bottom: -10,
borderRadius: 30,
backgroundColor: "#fd1775",
}}
color="white"
enableShadow
onPress={() => {
setIsAddVisible(true);
}}
>
<View row centerV centerH>
<MaterialIcons name="add" size={22} color={"white"} />
<Text
white
style={{ fontSize: 16, fontFamily: "Manrope_600SemiBold" }}
>
New
</Text>
</View>
</Button>
</LinearGradient>
<AddBrainDump
addBrainDumpProps={{
isVisible: isAddVisible,
setIsVisible: setIsAddVisible,
}}
/>
</View>
);
};
const styles = StyleSheet.create({
searchField: {
borderWidth: 1,
borderWidth: 0.7,
borderColor: "#9b9b9b",
borderRadius: 18,
height: 48,
borderRadius: 15,
height: 42,
paddingLeft: 10,
marginVertical: 20,
},

View File

@ -1,7 +1,10 @@
import { View, Text } from "react-native-ui-lib";
import React, { useState } from "react";
import { IBrainDump } from "@/contexts/DumpContext";
import { TouchableOpacity, TouchableWithoutFeedback } from "react-native-gesture-handler";
import {
TouchableOpacity,
TouchableWithoutFeedback,
} from "react-native-gesture-handler";
import MoveBrainDump from "./MoveBrainDump";
const BrainDumpItem = (props: { item: IBrainDump }) => {
@ -13,16 +16,34 @@ const BrainDumpItem = (props: { item: IBrainDump }) => {
<View
backgroundColor="white"
marginV-5
padding-15
style={{ borderRadius: 20, elevation: 2 }}
paddingH-13
paddingV-10
style={{ borderRadius: 15, elevation: 2 }}
>
<Text text70BL marginB-8>
<Text
text70B
style={{ fontSize: 15, fontFamily: "Manrope_600SemiBold" }}
marginB-8
>
{props.item.title}
</Text>
<Text text70>{props.item.description}</Text>
<Text
text70
style={{
fontSize: 13,
fontFamily: "Manrope_400Regular",
color: "#5c5c5c",
}}
>
{props.item.description}
</Text>
</View>
</TouchableWithoutFeedback>
<MoveBrainDump item={props.item} isVisible={isVisible} setIsVisible={setIsVisible} />
<MoveBrainDump
item={props.item}
isVisible={isVisible}
setIsVisible={setIsVisible}
/>
</View>
);
};

View File

@ -1,24 +1,33 @@
import { View } from 'react-native-ui-lib';
import React from 'react';
import { useBrainDumpContext } from '@/contexts/DumpContext';
import { FlatList } from 'react-native';
import BrainDumpItem from './DumpItem';
import { View } 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";
const DumpList = (props: { searchText: string }) => {
const { brainDumps } = useBrainDumpContext();
const filteredBrainDumps = props.searchText.trim() === ""
? brainDumps
: brainDumps.filter((item) =>
item.title.toLowerCase().includes(props.searchText.toLowerCase())
);
const filteredBrainDumps =
props.searchText.trim() === ""
? brainDumps
: brainDumps.filter(
(item) =>
item.title.toLowerCase().includes(props.searchText.toLowerCase()) ||
item.description
.toLowerCase()
.includes(props.searchText.toLowerCase())
);
return (
<View>
<View marginB-70>
<FlatList
style={{ zIndex: -1 }}
data={filteredBrainDumps}
keyExtractor={(item) => item.title}
renderItem={({ item }) => <BrainDumpItem key={item.title} item={item} />}
renderItem={({ item }) => (
<BrainDumpItem key={item.title} item={item} />
)}
/>
</View>
);

View File

@ -1,50 +1,67 @@
import React, { useState } from "react";
import { Button, Dialog, View, Text, TextField } from "react-native-ui-lib";
import { StyleSheet } from "react-native";
import React, { useEffect, useState } from "react";
import {
Button,
Dialog,
View,
Text,
TextField,
TouchableOpacity,
} from "react-native-ui-lib";
import { Dimensions, StyleSheet } from "react-native";
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
import { IBrainDump } from "@/contexts/DumpContext";
import { Entypo, EvilIcons, Feather, Octicons } from "@expo/vector-icons";
import { TouchableOpacity } from "react-native-gesture-handler";
import { IBrainDump, useBrainDumpContext } from "@/contexts/DumpContext";
import PenIcon from "@/assets/svgs/PenIcon";
import BinIcon from "@/assets/svgs/BinIcon";
import DropModalIcon from "@/assets/svgs/DropModalIcon";
import CloseXIcon from "@/assets/svgs/CloseXIcon";
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import RemindersIcon from "@/assets/svgs/RemindersIcon";
import MenuIcon from "@/assets/svgs/MenuIcon";
const MoveBrainDump = (props: {
item: IBrainDump;
isVisible: boolean;
setIsVisible: (value: boolean) => void;
}) => {
const { updateBrainDumpItem, deleteBrainDump } = useBrainDumpContext();
const [description, setDescription] = useState<string>(
props.item.description
);
const { width } = Dimensions.get("screen");
useEffect(() => {
updateBrainDumpItem(props.item.id, { description: description });
}, [description]);
return (
<Dialog
bottom={true}
height={"90%"}
width={width}
panDirection={PanningDirectionsEnum.DOWN}
onDismiss={() => props.setIsVisible(false)}
containerStyle={{
borderRadius: 10,
borderRadius: 15,
backgroundColor: "white",
width: "100%",
alignSelf: "stretch",
padding: 0,
padding: 10,
paddingTop: 3,
margin: 0,
}}
visible={props.isVisible}
>
<View row spread paddingH-10 paddingV-15>
<View center paddingT-8>
<TouchableOpacity onPress={() => props.setIsVisible(false)}>
<DropModalIcon />
</TouchableOpacity>
</View>
<View row spread paddingH-10 paddingB-15>
<Button
color="#05a8b6"
style={styles.topBtn}
iconSource={() => <EvilIcons name="close" size={30} color="black" />}
onPress={() => {
props.setIsVisible(false);
}}
/>
<Button
style={styles.topBtn}
iconSource={() => (
<Feather name="chevron-down" size={24} color="black" />
)}
iconSource={() => <CloseXIcon />}
onPress={() => {
props.setIsVisible(false);
}}
@ -52,34 +69,42 @@ const MoveBrainDump = (props: {
<View row>
<Button
style={styles.topBtn}
iconSource={() => (
<Octicons name="pencil" size={24} color="#919191" />
)}
onPress={() => {}}
marginR-10
iconSource={() => <PenIcon />}
onPress={() => {
console.log("selview");
props.setIsVisible(false);
}}
/>
<Button
style={styles.topBtn}
iconSource={() => (
<EvilIcons name="trash" size={30} color="#919191" />
)}
onPress={() => {}}
marginL-5
iconSource={() => <BinIcon />}
onPress={() => {
deleteBrainDump(props.item.id);
props.setIsVisible(false);
}}
/>
</View>
</View>
<View centerH>
<Text text60R>{props.item.title} </Text>
<Text style={styles.title}>{props.item.title} </Text>
</View>
<View style={styles.divider} />
<View row marginH-20>
<Entypo
name="text"
size={24}
color="black"
style={{ marginBottom: "auto" }}
/>
<View row gap-5>
<View paddingT-8 marginR-5>
<MenuIcon width={20} height={12} />
</View>
<TextField
textAlignVertical="top"
multiline
fieldStyle={{
width: "94%",
}}
style={{
fontFamily: "Manrope_400Regular",
fontSize: 14,
}}
placeholder="Add description"
numberOfLines={3}
value={description}
@ -89,6 +114,38 @@ const MoveBrainDump = (props: {
/>
</View>
<View style={styles.divider} />
<View gap-20>
<TouchableOpacity>
<View row centerV>
<NavToDosIcon
width={22}
color={"#919191"}
style={styles.optionsIcon}
/>
<Text style={styles.optionsReg}>Move to</Text>
<Text style={styles.optionsBold}> my to do's</Text>
</View>
</TouchableOpacity>
<TouchableOpacity>
<View row centerV>
<NavCalendarIcon
width={22}
height={22}
color={"#919191"}
style={styles.optionsIcon}
/>
<Text style={styles.optionsReg}>Move to</Text>
<Text style={styles.optionsBold}> my calendar</Text>
</View>
</TouchableOpacity>
<TouchableOpacity>
<View row centerV>
<RemindersIcon width={22} style={styles.optionsIcon} />
<Text style={styles.optionsReg}>Move to</Text>
<Text style={styles.optionsBold}> my reminders</Text>
</View>
</TouchableOpacity>
</View>
</Dialog>
);
};
@ -113,12 +170,28 @@ const styles = StyleSheet.create({
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
marginTop: -3,
},
rotateSwitch: {
marginLeft: 35,
marginBottom: 10,
marginTop: 25,
},
optionsReg: {
fontSize: 16,
fontFamily: "PlusJakartaSans_400Regular",
},
optionsBold: {
fontSize: 16,
fontFamily: "PlusJakartaSans_600SemiBold",
},
optionsIcon: {
marginRight: 10,
},
title: {
fontSize: 22,
fontFamily: "Manrope_500Medium",
},
});
export default MoveBrainDump;

View File

@ -14,12 +14,14 @@ import {
Text,
View,
} from "react-native-ui-lib";
import { TouchableOpacity } from "react-native";
import { StyleSheet, TouchableOpacity } from "react-native";
import { ManuallyAddEventModal } from "@/components/pages/calendar/ManuallyAddEventModal";
import AddChoreDialog from "../todos/AddChoreDialog";
import { ToDosContextProvider } from "@/contexts/ToDosContext";
import UploadImageDialog from "./UploadImageDialog";
import CameraIcon from "@/assets/svgs/CameraIcon";
import CalendarIcon from "@/assets/svgs/CalendarIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
export const AddEventDialog = () => {
const [show, setShow] = useState(false);
@ -37,127 +39,150 @@ export const AddEventDialog = () => {
const handleScanImageDialog = () => {
setShow(false);
setTimeout(() => {
setShowUploadDialog(true);
setShowUploadDialog(true);
}, 100);
}
};
return (
<ToDosContextProvider>
<>
<Button
style={{
position: "absolute",
bottom: 20,
right: 20,
height: 40,
borderRadius: 30,
backgroundColor: "#fd1775",
alignItems: "center",
justifyContent: "center",
}}
centerV
color="white"
enableShadow
iconSource={() => (
<MaterialIcons name="add" size={22} color={"white"} />
)}
onPress={() => setShow(true)}
label="New"
text60R
/>
<Dialog
visible={show}
onDismiss={() => setShow(false)}
panDirection={PanningProvider.Directions.DOWN}
center
>
<Card
<>
<Button
style={{
paddingHorizontal: 40,
paddingTop: 40,
paddingBottom: 20,
justifyContent: "center",
position: "absolute",
bottom: 20,
right: 20,
height: 40,
borderRadius: 30,
backgroundColor: "#fd1775",
alignItems: "center",
justifyContent: "center",
}}
color="white"
enableShadow
onPress={() => setShow(true)}
>
<Text text50R>Create a new event</Text>
<View style={{ marginTop: 20, alignItems: "center", width: "100%" }}>
<Button
style={{
marginBottom: 10,
backgroundColor: "#ea156c",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Scan Image"
onPress={handleScanImageDialog}
iconSource={() => (
<Feather
name="camera"
size={21}
style={{ marginRight: 7 }}
color="white"
/>
)}
/>
<Button
style={{
marginBottom: 10,
backgroundColor: "#e28800",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Create Event"
onPress={handleOpenManualInputModal}
iconSource={() => (
<MaterialCommunityIcons
name="calendar-text-outline"
size={22}
style={{ marginRight: 5 }}
color="white"
/>
)}
/>
<Button
style={{
marginBottom: 10,
backgroundColor: "#05a8b6",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Add To Do"
onPress={() => setChoreDialogVisible(true)}
iconSource={() => (
<AntDesign
name="checkcircleo"
size={20}
style={{ marginRight: 7 }}
color="white"
/>
)}
/>
<View row centerV centerH>
<MaterialIcons name="add" size={22} color={"white"} />
<Text white style={{ fontSize: 16, fontFamily: 'Manrope_600SemiBold' }}>
New
</Text>
</View>
</Button>
<TouchableOpacity onPress={() => setShow(false)}>
<Text style={{ marginTop: 20, color: "#999999" }} text70>Go back to calendar</Text>
</TouchableOpacity>
</Card>
</Dialog>
<AddChoreDialog isVisible={choreDialogVisible} setIsVisible={setChoreDialogVisible} />
<ManuallyAddEventModal
show={showManualInputModal}
close={() => setShowManualInputModal(false)}
/>
<UploadImageDialog show={showUploadDialog} setShow={setShowUploadDialog} />
</>
<Dialog
visible={show}
onDismiss={() => setShow(false)}
panDirection={PanningProvider.Directions.DOWN}
center
>
<Card style={styles.dialogCard}>
<Text text60 style={styles.modalTitle}>
Create a new event
</Text>
<View
style={{ marginTop: 20, alignItems: "center", width: "100%" }}
>
<Button
style={{
marginBottom: 10,
backgroundColor: "#ea156c",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Scan Image"
labelStyle={styles.btnLabel}
onPress={handleScanImageDialog}
iconSource={() => (
<CameraIcon color="white" style={styles.btnIcon} />
)}
/>
<Button
style={{
marginBottom: 10,
backgroundColor: "#e28800",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Create Event"
labelStyle={styles.btnLabel}
onPress={handleOpenManualInputModal}
iconSource={() => (
<CalendarIcon color={"white"} style={styles.btnIcon} />
)}
/>
<Button
style={{
marginBottom: 10,
backgroundColor: "#05a8b6",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Add To Do"
labelStyle={styles.btnLabel}
onPress={() => setChoreDialogVisible(true)}
iconSource={() => (
<NavToDosIcon
color={"white"}
width={23}
style={styles.btnIcon}
/>
)}
/>
</View>
<TouchableOpacity onPress={() => setShow(false)}>
<Text style={styles.bottomText} text70>
Go back to calendar
</Text>
</TouchableOpacity>
</Card>
</Dialog>
<AddChoreDialog
isVisible={choreDialogVisible}
setIsVisible={setChoreDialogVisible}
/>
<ManuallyAddEventModal
show={showManualInputModal}
close={() => setShowManualInputModal(false)}
/>
<UploadImageDialog
show={showUploadDialog}
setShow={setShowUploadDialog}
/>
</>
</ToDosContextProvider>
);
};
const styles = StyleSheet.create({
modalTitle: {
fontSize: 22,
fontFamily: "Manrope_600SemiBold",
marginBottom: 16,
},
bottomText: {
marginTop: 20,
color: "#999999",
fontSize: 13.53,
fontFamily: "Poppins_500Medium",
},
dialogCard: {
paddingHorizontal: 40,
paddingTop: 35,
paddingBottom: 20,
justifyContent: "center",
alignItems: "center",
borderRadius: 20,
},
btnLabel: {
fontSize: 15,
fontFamily: "PlusJakartaSans_500Medium",
},
btnIcon: { marginRight: 10 },
});

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useRef, useState } from "react";
import { LayoutChangeEvent, StyleSheet } from "react-native";
import { Calendar } from "react-native-big-calendar";
import {
@ -6,18 +6,17 @@ import {
PickerModes,
SegmentedControl,
View,
Text
} from "react-native-ui-lib";
import { MaterialIcons } from "@expo/vector-icons";
import { AddEventDialog } from "@/components/pages/calendar/AddEventDialog";
import { useAuthContext } from "@/contexts/AuthContext";
import HeaderTemplate from "@/components/shared/HeaderTemplate";
import CalendarViewSwitch from "@/components/pages/calendar/CalendarViewSwitch";
import { ManuallyAddEventModal } from "@/components/pages/calendar/ManuallyAddEventModal";
import { CalendarEvent, useCalendarContext } from "@/contexts/CalendarContext";
import { CalendarEvent } from "@/contexts/CalendarContext";
import { useSettingsContext } from "@/contexts/SettingsContext";
import EditEventDialog from "./EditEventDialog";
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import { useGetEvents } from "@/hooks/firebase/useGetEvents";
import { Text } from "react-native-ui-lib";
const modeMap = new Map([
[0, "day"],
@ -47,15 +46,22 @@ export default function CalendarPage() {
const styles = StyleSheet.create({
segmentslblStyle: {
fontSize: 14,
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},
calHeader: {
borderWidth: 0,
},
dayModeHeader: {
alignSelf: "flex-start",
justifyContent: "space-between",
alignContent: "center",
width: 38,
right: 42,
},
});
const { profileData } = useAuthContext();
const [isFamilyView, setIsFamilyView] = useState<boolean>(true);
const [isFamilyView, setIsFamilyView] = useState<boolean>(false);
const [calendarHeight, setCalendarHeight] = useState(0);
const [mode, setMode] = useState<"week" | "month" | "day">("week");
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
@ -64,8 +70,7 @@ export default function CalendarPage() {
>(undefined);
const calendarContainerRef = useRef(null);
// const { events, familyEvents } = useCalendarContext();
const {data: events} = useGetEvents()
const { data: events } = useGetEvents(isFamilyView);
const onLayout = (event: LayoutChangeEvent) => {
const { height } = event.nativeEvent.layout;
@ -80,22 +85,22 @@ export default function CalendarPage() {
};
const handleMonthChange = (month: string) => {
// Get the current day and year
const currentDay = selectedDate.getDate();
const currentYear = selectedDate.getFullYear();
// Find the new month index
const newMonthIndex = months.indexOf(month);
// Create a new Date object with the selected month, preserving day and year
const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
// Update both selectedDate and activeDate in the Calendar component
setSelectedDate(updatedDate);
};
return (
<View style={{ flex: 1, height: "100%", padding: 10 }}>
<View
style={{ flex: 1, height: "100%", padding: 10 }}
paddingH-22
paddingT-0
>
<HeaderTemplate
message={"Let's get your week started!"}
isWelcome={true}
@ -120,17 +125,27 @@ export default function CalendarPage() {
marginBottom: 10,
}}
>
<Picker
value={months[selectedDate.getMonth()]} // Get the month from the date
placeholder={"Select Month"}
mode={PickerModes.SINGLE}
onChange={(itemValue) => handleMonthChange(itemValue as string)}
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"} />}
>
{months.map((month) => (
<Picker.Item key={month} label={month} value={month} />
))}
</Picker>
<View row centerV gap-3>
<Text style={{ fontFamily: "Manrope_500Medium", fontSize: 17 }}>
{selectedDate.getFullYear()}
</Text>
<Picker
value={months[selectedDate.getMonth()]} // Get the month from the date
placeholder={"Select Month"}
style={{ fontFamily: "Manrope_500Medium", fontSize: 17 }}
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>
<View>
<SegmentedControl
@ -141,7 +156,6 @@ export default function CalendarPage() {
activeColor="white"
outlineColor="white"
outlineWidth={3}
style={{ backgroundColor: "green" }}
segmentLabelStyle={styles.segmentslblStyle}
onChangeIndex={handleSegmentChange}
initialIndex={mode === "day" ? 0 : mode === "week" ? 1 : 2}
@ -154,7 +168,7 @@ export default function CalendarPage() {
bodyContainerStyle={styles.calHeader}
mode={mode}
events={isFamilyView ? events ?? [] : events ?? []}
eventCellStyle={{ backgroundColor: isFamilyView ? '#46a80a' : calendarColor }}
eventCellStyle={(event) => ({ backgroundColor: event.eventColor })}
onPressEvent={(event) => {
setEditVisible(true);
setEventForEdit(event);
@ -163,6 +177,7 @@ export default function CalendarPage() {
activeDate={selectedDate}
date={selectedDate}
onPressCell={setSelectedNewEndDate}
headerContentStyle={mode === "day" ? styles.dayModeHeader : {}}
/>
)}
</View>

View File

@ -44,7 +44,7 @@ const CalendarViewSwitch = (calendarViewProps: ICalendarViewProps) => {
paddingH-15
style={calView ? styles.switchBtnActive : styles.switchBtn}
>
<Text color={calView ? "white" : "#a1a1a1"} text70R>
<Text color={calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
Family View
</Text>
</View>
@ -63,7 +63,7 @@ const CalendarViewSwitch = (calendarViewProps: ICalendarViewProps) => {
paddingH-15
style={!calView ? styles.switchBtnActive : styles.switchBtn}
>
<Text color={!calView ? "white" : "#a1a1a1"} text70R>
<Text color={!calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
My View
</Text>
</View>
@ -83,4 +83,8 @@ const styles = StyleSheet.create({
backgroundColor: "white",
borderRadius: 50,
},
switchTxt:{
fontSize: 16,
fontFamily: 'Manrope_600SemiBold'
}
});

View File

@ -1,7 +1,5 @@
import { View, Text, Button, Switch } from "react-native-ui-lib";
import React, { useEffect, useState } from "react";
import PointsSlider from "@/components/shared/PointsSlider";
import { repeatOptions, useToDosContext } from "@/contexts/ToDosContext";
import { Feather, AntDesign, Ionicons } from "@expo/vector-icons";
import {
Dialog,
@ -13,11 +11,11 @@ import {
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
import { StyleSheet } from "react-native";
import DropModalIcon from "@/assets/svgs/DropModalIcon";
import { CalendarEvent, useCalendarContext } from "@/contexts/CalendarContext";
import { CalendarEvent } from "@/contexts/CalendarContext";
import ClockIcon from "@/assets/svgs/ClockIcon";
import LockIcon from "@/assets/svgs/LockIcon";
import MenuIcon from "@/assets/svgs/MenuIcon";
import { eventCellCss } from "react-native-big-calendar";
import { useUpdateEvent } from "@/hooks/firebase/useUpdateEvent";
interface IEditEventDialog {
event: CalendarEvent;
@ -25,9 +23,10 @@ interface IEditEventDialog {
setIsVisible: (value: boolean) => void;
}
const EditEventDialog = (editEventProps: IEditEventDialog) => {
const { updateEvent } = useCalendarContext();
const [event, setEvent] = useState<CalendarEvent>(editEventProps.event);
const { mutateAsync: updateEvent } = useUpdateEvent();
useEffect(() => {
setEvent(editEventProps.event);
}, [editEventProps.isVisible]);
@ -72,8 +71,7 @@ const EditEventDialog = (editEventProps: IEditEventDialog) => {
onPress={() => {
try {
if (event.id) {
updateEvent(event, event.id);
editEventProps.setIsVisible(false);
updateEvent(event).then(() => editEventProps.setIsVisible(false));
}
} catch (error) {
console.error(error);
@ -125,6 +123,7 @@ const EditEventDialog = (editEventProps: IEditEventDialog) => {
value={event.start}
text70
marginL-8
maximumDate={event.end}
onChange={(date) => {
setEvent((prev) => ({ ...prev, start: date }));
}}
@ -136,6 +135,11 @@ const EditEventDialog = (editEventProps: IEditEventDialog) => {
onChange={(date) => {
setEvent((prev) => ({ ...prev, start: date }));
}}
maximumDate={event.end}
dateTimeFormatter={(date, mode) => date.toLocaleTimeString("en-us",
{ hour: "numeric",
minute: "numeric"
})}
mode="time"
marginR-30
/>
@ -147,6 +151,7 @@ const EditEventDialog = (editEventProps: IEditEventDialog) => {
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={event.end}
minimumDate={event.start}
text70
marginL-8
onChange={(date) => {
@ -157,9 +162,14 @@ const EditEventDialog = (editEventProps: IEditEventDialog) => {
<DateTimePicker
text70
value={event.end}
minimumDate={event.start}
onChange={(date) => {
setEvent((prev) => ({ ...prev, end: date }));
}}
dateTimeFormatter={(date, mode) => date.toLocaleTimeString("en-us",
{ hour: "numeric",
minute: "numeric"
})}
mode="time"
marginR-30
/>

View File

@ -30,11 +30,12 @@ import { addHours, setDate } from "date-fns";
import DropModalIcon from "@/assets/svgs/DropModalIcon";
import { CalendarEvent, useCalendarContext } from "@/contexts/CalendarContext";
import { repeatOptions } from "@/contexts/ToDosContext";
import { StyleSheet } from "react-native";
import { ImageBackground, StyleSheet } from "react-native";
import ClockIcon from "@/assets/svgs/ClockIcon";
import LockIcon from "@/assets/svgs/LockIcon";
import MenuIcon from "@/assets/svgs/MenuIcon";
import CameraIcon from "@/assets/svgs/CameraIcon";
import AssigneesDisplay from "@/components/shared/AssigneesDisplay";
const daysOfWeek = [
{ label: "Monday", value: "monday" },
@ -69,7 +70,9 @@ export const ManuallyAddEventModal = ({
return date;
});
const [endTime, setEndTime] = useState(() => {
const date = initialDate ? addHours(initialDate, 1) : new Date();
const date = initialDate
? addHours(initialDate, 1)
: addHours(new Date(), 1);
date.setSeconds(0, 0);
return date;
});
@ -81,8 +84,9 @@ export const ManuallyAddEventModal = ({
const { mutateAsync: createEvent, isLoading, isError } = useCreateEvent();
const formatDateTime = (date: Date) => {
return date.toLocaleDateString("en-US", {
const formatDateTime = (date?: Date | string) => {
if (!date) return undefined;
return new Date(date).toLocaleDateString("en-US", {
weekday: "long",
month: "short",
day: "numeric",
@ -98,30 +102,26 @@ export const ManuallyAddEventModal = ({
return combined;
};
const handleSave = () => {
const handleSave = async () => {
let finalStartDate: Date;
let finalEndDate: Date;
if (isAllDay) {
finalStartDate = new Date(startDate);
finalStartDate.setHours(0, 0, 0, 0);
finalEndDate = new Date(startDate);
finalEndDate.setHours(23, 59, 59, 999);
finalStartDate = new Date(startDate.setHours(0, 0, 0, 0));
finalEndDate = new Date(startDate.setHours(0, 0, 0, 0));
} else {
finalStartDate = combineDateAndTime(startDate, startTime);
finalEndDate = combineDateAndTime(endDate, endTime);
}
const eventData: CalendarEvent = {
const eventData: Partial<EventData> = {
title: title,
start: finalStartDate,
end: finalEndDate,
startDate: finalStartDate,
endDate: finalEndDate,
allDay: isAllDay,
private: isPrivate,
};
addEvent(eventData);
await createEvent(eventData);
close();
};
@ -196,13 +196,27 @@ export const ManuallyAddEventModal = ({
}}
>
<TouchableOpacity onPress={close}>
<Text style={{ color: "#05a8b6" }} text70>
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Cancel
</Text>
</TouchableOpacity>
<DropModalIcon onPress={close} />
<TouchableOpacity onPress={handleSave}>
<Text style={{ color: "#05a8b6" }} text70>
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Save
</Text>
</TouchableOpacity>
@ -215,16 +229,22 @@ export const ManuallyAddEventModal = ({
setTitle(text);
}}
placeholderTextColor="#2d2d30"
text60R
marginT-15
marginL-30
style={{ fontFamily: "Manrope_500Medium", fontSize: 22 }}
paddingT-15
paddingL-30
/>
<View style={styles.divider} marginT-8 />
<View marginL-30 centerV>
<View row spread marginB-10 centerV>
<View row>
<AntDesign name="clockcircleo" size={24} color="#919191" />
<Text text70 marginL-10>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
All day
</Text>
</View>
@ -243,38 +263,86 @@ export const ManuallyAddEventModal = ({
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={startDate}
text70
marginL-8
onChange={(date) => {
setStartDate(date);
}}
maximumDate={endDate}
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-8
/>
</View>
<DateTimePicker text70 value={startDate} mode="time" marginR-30/>
<DateTimePicker
value={startTime}
onChange={(date) => setStartTime(date)}
maximumDate={endTime}
minuteInterval={5}
dateTimeFormatter={(date, mode) =>
date.toLocaleTimeString("en-us", {
hour: "numeric",
minute: "numeric",
})
}
mode="time"
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginR-30
/>
</View>
{!isAllDay && <View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191" />
{!isAllDay && (
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={endDate}
minimumDate={startDate}
text70
marginL-8
onChange={(date) => {
setEndDate(date);
}}
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
/>
</View>
<DateTimePicker
value={endDate}
text70
marginL-8
onChange={(date) => {
setEndDate(date);
value={endTime}
onChange={(date) => setEndTime(date)}
minimumDate={startTime}
minuteInterval={5}
dateTimeFormatter={(date, mode) =>
date.toLocaleTimeString("en-us", {
hour: "numeric",
minute: "numeric",
})
}
mode="time"
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginR-30
/>
</View>
<DateTimePicker text70 value={endDate} mode="time" marginR-30/>
</View>}
)}
</View>
<View style={styles.divider} />
<View marginH-30 marginB-10 row centerV>
<Ionicons name="person-circle-outline" size={28} color="#919191" />
<Text text70R marginL-10>
Assignees
<Text
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 18 }}
marginL-10
>
Attendees
</Text>
<Button
size={ButtonSize.small}
@ -290,35 +358,25 @@ export const ManuallyAddEventModal = ({
borderWidth: 1,
}}
color="#ea156c"
label="Assign"
label="Add"
labelStyle={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
/>
</View>
<View row marginH-13 marginT-13>
<View
marginL-30
style={{
aspectRatio: 1,
width: 50,
backgroundColor: "red",
borderRadius: 50,
}}
/>
<View
marginL-30
style={{
aspectRatio: 1,
width: 50,
backgroundColor: "red",
borderRadius: 50,
}}
/>
<View marginL-35>
<AssigneesDisplay />
</View>
<View style={styles.divider} />
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<ClockIcon />
<Text text70 marginL-10>
Reminder
<Text
style={{
fontFamily: "Manrope_600SemiBold",
fontSize: 18,
}}
marginL-10
>
Reminders
</Text>
</View>
<View>
@ -335,6 +393,7 @@ export const ManuallyAddEventModal = ({
borderColor: "#ea156c",
borderWidth: 1,
}}
labelStyle={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
color="#ea156c"
label="Set Reminder"
/>
@ -344,7 +403,13 @@ export const ManuallyAddEventModal = ({
<View marginH-30 marginB-0 row spread centerV>
<View row>
<LockIcon />
<Text text70 marginL-10>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
Mark as Private
</Text>
</View>
@ -362,7 +427,13 @@ export const ManuallyAddEventModal = ({
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<MenuIcon />
<Text text70 marginL-10>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
Add Details
</Text>
</View>
@ -374,6 +445,8 @@ export const ManuallyAddEventModal = ({
marginB-15
label="Create event from image"
text70
style={{ height: 47 }}
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium", fontSize: 15 }}
backgroundColor="#05a8b6"
iconSource={() => (
<View marginR-5>

View File

@ -11,6 +11,8 @@ import { Dialog, PanningProvider, Card } from "react-native-ui-lib";
import { StyleSheet } from "react-native";
import { Feather, MaterialIcons } from "@expo/vector-icons";
import * as ImagePicker from "expo-image-picker";
import AddImageIcon from "@/assets/svgs/AddImageIcon";
import CameraIcon from "@/assets/svgs/CameraIcon";
interface IUploadDialogProps {
show: boolean;
@ -51,19 +53,9 @@ const UploadImageDialog = (uploadDialogProps: IUploadDialogProps) => {
panDirection={PanningProvider.Directions.DOWN}
center
>
<Card
style={{
paddingHorizontal: 40,
paddingTop: 20,
paddingBottom: 10,
justifyContent: "center",
alignItems: "center",
}}
>
<Card style={styles.modalCard}>
<View centerH>
<Text text60 marginB-20>
Upload an Image
</Text>
<Text style={styles.modalTitle}>Upload an Image</Text>
{!selectedImage && (
<TouchableOpacity onPress={handleImagePick}>
<View
@ -73,12 +65,8 @@ const UploadImageDialog = (uploadDialogProps: IUploadDialogProps) => {
gap-8
marginB-20
>
<MaterialIcons
name="add-photo-alternate"
size={30}
color="#fd1775"
/>
<Text color="#fd1775" text70>
<AddImageIcon />
<Text style={styles.uploadTxt}>
Click here to upload an image
</Text>
</View>
@ -102,38 +90,31 @@ const UploadImageDialog = (uploadDialogProps: IUploadDialogProps) => {
setImageTitle("");
}}
>
<Feather
name="trash"
size={22}
color="#919191"
/>
<Feather name="trash" size={22} color="#919191" />
</TouchableOpacity>
</View>
<Button
style={{
marginBottom: 10,
marginTop: 20,
marginTop: 35,
backgroundColor: "#ea156c",
justifyContent: "center",
paddingVertical: 13,
paddingVertical: 15,
width: 285,
alignItems: "center",
}}
label="Upload Image"
onPress={() => {}}
labelStyle={styles.btnLbl}
iconSource={() => (
<Feather
name="camera"
size={21}
style={{ marginRight: 7 }}
color="white"
/>
<CameraIcon color="white" style={{marginRight: 10}}/>
)}
/>
</>
)}
<TouchableOpacity onPress={() => uploadDialogProps.setShow(false)}>
<Text text80 color="#999999">
<Text style={styles.bottomText}>
Go back
</Text>
</TouchableOpacity>
@ -148,6 +129,7 @@ export default UploadImageDialog;
const styles = StyleSheet.create({
uploadImgBox: {
backgroundColor: "#ffe8f2",
padding: 35,
width: "100%",
aspectRatio: 1.8,
borderRadius: 20,
@ -155,10 +137,20 @@ const styles = StyleSheet.create({
borderColor: "#fd1775",
borderStyle: "dashed",
},
btnLbl: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 15,
},
uploadTxt: {
color: "#b11d5a",
fontSize: 15,
fontFamily: "PlusJakartaSans_400Regular",
marginTop: 12
},
selectedImage: {
width: 60,
width: 38.69,
aspectRatio: 1,
borderRadius: 10,
borderRadius: 5,
},
imageContainer: {
alignItems: "center",
@ -166,13 +158,33 @@ const styles = StyleSheet.create({
borderWidth: 1,
borderColor: "#d9d9d9",
padding: 10,
borderRadius: 13,
borderRadius: 10,
},
imageInfo: {
marginLeft: 10,
},
imageTitle: {
fontSize: 16,
color: "#333",
fontSize: 15,
color: "#262627",
fontFamily: "PlusJakartaSans_400Regular"
},
modalCard: {
paddingHorizontal: 25,
paddingTop: 30,
paddingBottom: 17,
justifyContent: "center",
alignItems: "center",
borderRadius: 20,
},
modalTitle: {
fontSize: 22,
fontFamily: "Manrope_600SemiBold",
marginBottom: 20,
},
bottomText: {
marginTop: 20,
color: "#999999",
fontSize: 13.53,
fontFamily: "Poppins_500Medium",
},
});

View File

@ -45,8 +45,11 @@ const AddGroceryItem = () => {
text70L
iconSource={() => <FontAwesome6 name="add" size={18} color="white" />}
style={styles.finishShopBtn}
labelStyle={styles.addBtnLbl}
enableShadow
onPress={() => {setIsAddingGrocery(true)}}
onPress={() => {
setIsAddingGrocery(true);
}}
/>
</View>
</View>
@ -89,4 +92,5 @@ const styles = StyleSheet.create({
flex: 1,
marginHorizontal: 3,
},
addBtnLbl: { fontFamily: "Manrope_500Medium", fontSize: 17, marginLeft: 5 },
});

View File

@ -1,5 +1,5 @@
import { StyleSheet } from "react-native";
import React, { useState } from "react";
import React from "react";
import {
Dialog,
Text,
@ -11,9 +11,9 @@ import {
} from "react-native-ui-lib";
import {
GroceryFrequency,
IGrocery,
useGroceryContext,
} from "@/contexts/GroceryContext";
import { IGrocery } from "@/hooks/firebase/types/groceryData";
interface EditGroceryFrequencyProps {
visible: boolean;
onClose: () => void;
@ -42,7 +42,7 @@ const EditGroceryFrequency = (props: EditGroceryFrequencyProps) => {
<Switch
value={props.item.recurring}
onValueChange={(value) =>
updateGroceryItem(props.item.id, { recurring: value })
updateGroceryItem({id: props.item.id, recurring: value})
}
onColor={"lime"}
/>
@ -56,8 +56,9 @@ const EditGroceryFrequency = (props: EditGroceryFrequencyProps) => {
const selectedFrequency =
GroceryFrequency[item as keyof typeof GroceryFrequency];
if (selectedFrequency) {
updateGroceryItem(props.item.id, {
frequency: selectedFrequency,
updateGroceryItem({
id: props.item.id,
frequency: selectedFrequency,
});
} else {
console.error("Invalid frequency selected");

View File

@ -1,60 +1,72 @@
import { View, Text } from "react-native";
import React, { useEffect, useState } from "react";
import { TextField } from "react-native-ui-lib";
import {
GroceryCategory,
IGrocery,
useGroceryContext,
} from "@/contexts/GroceryContext";
import { TouchableWithoutFeedback } from "react-native-gesture-handler";
import {Text, View} from "react-native";
import React, {useEffect, useRef} from "react";
import {TextField, TextFieldRef} from "react-native-ui-lib";
import {GroceryCategory, useGroceryContext,} from "@/contexts/GroceryContext";
interface IEditGrocery {
id?: number;
title: string;
setTitle: (value: string) => void;
setCategory?: (category: GroceryCategory) => void;
category: GroceryCategory;
setSubmit?: (value: boolean) => void;
updateCategory?: (id: number, changes: Partial<IGrocery>) => void;
closeEdit?: (value: boolean) => void;
id?: string;
title: string;
category: GroceryCategory;
setTitle: (value: string) => void;
setCategory?: (category: GroceryCategory) => void;
setSubmit?: (value: boolean) => void;
closeEdit?: (value: boolean) => void;
handleEditSubmit?: Function
}
const EditGroceryItem = ({ editGrocery }: { editGrocery: IEditGrocery }) => {
const { fuzzyMatchGroceryCategory } = useGroceryContext();
const EditGroceryItem = ({editGrocery}: { editGrocery: IEditGrocery }) => {
const {fuzzyMatchGroceryCategory} = useGroceryContext();
const inputRef = useRef<TextFieldRef>(null);
useEffect(() => {
if (editGrocery.setCategory)
editGrocery.setCategory(fuzzyMatchGroceryCategory(editGrocery.title));
}, [editGrocery.title]);
useEffect(() => {
if (editGrocery.setCategory)
editGrocery.setCategory(fuzzyMatchGroceryCategory(editGrocery.title));
}, [editGrocery.title]);
return (
<View
style={{
backgroundColor: "white",
width: "100%",
borderRadius: 25,
padding: 15,
}}
>
<TextField
placeholder="Grocery"
value={editGrocery.title}
onChangeText={(value) => {
editGrocery.setTitle(value);
}}
onSubmitEditing={() => {
if (editGrocery.setSubmit) editGrocery.setSubmit(true);
if (editGrocery.closeEdit) editGrocery.closeEdit(false);
if (editGrocery.updateCategory && editGrocery.id)
editGrocery.updateCategory(editGrocery.id, {
category: fuzzyMatchGroceryCategory(editGrocery.title), title: editGrocery.title
});
}}
maxLength={25}
/>
<Text>{editGrocery.category}</Text>
</View>
);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus(); // Focus on the TextField
}
}, []);
return (
<View
style={{
backgroundColor: "white",
width: "100%",
borderRadius: 25,
padding: 15,
marginTop: 10
}}
>
<TextField
text70T
style={{fontWeight: "400"}}
ref={inputRef}
placeholder="Grocery"
value={editGrocery.title}
onChangeText={(value) => {
editGrocery.setTitle(value);
}}
onSubmitEditing={() => {
if (editGrocery.setSubmit) {
editGrocery.setSubmit(true);
}
if (editGrocery.setCategory) {
editGrocery.setCategory(fuzzyMatchGroceryCategory(editGrocery.title));
}
if (editGrocery.handleEditSubmit) {
editGrocery.handleEditSubmit({id: editGrocery.id, title: editGrocery.title, category: editGrocery.category});
}
if (editGrocery.closeEdit) {
editGrocery.closeEdit(false);
}
}}
maxLength={25}
/>
<Text>{editGrocery.category}</Text>
</View>
);
};
export default EditGroceryItem;

View File

@ -1,66 +1,68 @@
import {
View,
Text,
Button,
TouchableOpacity,
Checkbox,
ButtonSize,
} from "react-native-ui-lib";
import { Checkbox, Text, TouchableOpacity, View } from "react-native-ui-lib";
import React, { useEffect, useState } from "react";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { MaterialCommunityIcons, AntDesign } from "@expo/vector-icons";
import { ListItem } from "react-native-ui-lib";
import {
GroceryCategory,
IGrocery,
useGroceryContext,
} from "@/contexts/GroceryContext";
import { AntDesign } from "@expo/vector-icons";
import { GroceryCategory, useGroceryContext } from "@/contexts/GroceryContext";
import EditGroceryFrequency from "./EditGroceryFrequency";
import EditGroceryItem from "./EditGroceryItem";
import { StyleSheet } from "react-native";
import { ImageBackground, StyleSheet } from "react-native";
import { IGrocery } from "@/hooks/firebase/types/groceryData";
import firestore from "@react-native-firebase/firestore";
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
const GroceryItem = ({
item,
handleItemApproved,
}: {
item: IGrocery;
handleItemApproved: (id: number, changes: Partial<IGrocery>) => void;
handleItemApproved: (id: string, changes: Partial<IGrocery>) => void;
}) => {
const { updateGroceryItem, groceries } = useGroceryContext();
const { updateGroceryItem } = useGroceryContext();
const { profileType } = useAuthContext();
const [openFreqEdit, setOpenFreqEdit] = useState<boolean>(false);
const [isEditingTitle, setIsEditingTitle] = useState<boolean>(false);
const [newTitle, setNewTitle] = useState<string>("");
const [category, setCategory] = useState<GroceryCategory>(
GroceryCategory.None
);
const [itemCreator, setItemCreator] = useState<UserProfile>(null);
const handleTitleChange = (newTitle: string) => {
updateGroceryItem(item.id, { title: newTitle });
updateGroceryItem({ id: item?.id, title: newTitle });
};
const handleCategoryChange = (newCategory: GroceryCategory) => {
updateGroceryItem(item.id, { category: newCategory });
updateGroceryItem({ id: item?.id, category: newCategory });
};
useEffect(() => {
setNewTitle(item.title);
getItemCreator(item?.creatorId);
}, []);
const getItemCreator = async (uid: string | undefined) => {
if (uid) {
const documentSnapshot = await firestore()
.collection("Profiles")
.doc(uid)
.get();
if (documentSnapshot.exists) {
setItemCreator(documentSnapshot.data() as UserProfile);
}
}
};
return (
<View
key={item.id}
style={{ borderRadius: 18, marginVertical: 5 }}
style={{ borderRadius: 17, marginVertical: 5 }}
backgroundColor="white"
centerV
padding-0
paddingH-13
paddingV-10
>
<ListItem
onPress={() => {
setOpenFreqEdit(true);
}}
>
<View row spread>
<EditGroceryFrequency
visible={openFreqEdit}
key={item.id}
@ -69,90 +71,80 @@ const GroceryItem = ({
setOpenFreqEdit(false);
}}
/>
<ListItem.Part left containerStyle={{ flex: 1, paddingStart: 20 }}>
{!isEditingTitle ? (
<View>
<TouchableOpacity onPress={() => setIsEditingTitle(true)}>
<Text text70BL>{item.title}</Text>
</TouchableOpacity>
</View>
) : (
<EditGroceryItem
{!isEditingTitle ? (
<View>
<TouchableOpacity onPress={() => setIsEditingTitle(true)}>
<Text text70T black style={styles.title}>
{item.title}
</Text>
</TouchableOpacity>
</View>
) : (
<EditGroceryItem
editGrocery={{
id: item.id,
title: newTitle,
setTitle: setNewTitle,
category: category,
updateCategory: updateGroceryItem,
closeEdit: setIsEditingTitle,
setTitle: setNewTitle,
setCategory: setCategory,
closeEdit: setIsEditingTitle,
handleEditSubmit: updateGroceryItem
}}
/>
)}
{!item.approved ? (
<View row centerV marginB-10>
<AntDesign
name="check"
size={24}
style={{
color: item.approved ? "green" : "#aaaaaa",
marginRight: 15,
}}
onPress={() => {
handleItemApproved(item.id, { approved: true });
}}
/>
)}
</ListItem.Part>
<ListItem.Part right containerStyle={{ paddingEnd: item.approved ? 20 : 5 }}>
{!item.approved ? (
<View row >
<Button
padding-0
children={
<AntDesign
name="check"
size={24}
style={{
color: item.approved ? "green" : "#aaaaaa",
}}
/>
}
backgroundColor="transparent"
size={Button.sizes.small}
onPress={() => {
handleItemApproved(item.id, { approved: true });
}}
/>
<Button
padding-0
children={
<AntDesign
name="close"
size={24}
style={{ color: item.approved ? "#aaaaaa" : "red" }}
/>
}
backgroundColor="transparent"
size={Button.sizes.small}
onPress={() => {
handleItemApproved(item.id, { approved: false });
}}
/>
</View>
) : (
<Checkbox
value={item.bought}
style={styles.checkbox}
onValueChange={() =>
updateGroceryItem(item.id, { bought: !item.bought })
}
<AntDesign
name="close"
size={24}
style={{ color: item.approved ? "#aaaaaa" : "red" }}
onPress={() => {
handleItemApproved(item.id, { approved: false });
}}
/>
)}
</ListItem.Part>
</ListItem>
</View>
) : (
<Checkbox
value={item.bought}
containerStyle={styles.checkbox}
hitSlop={20}
onValueChange={() =>
updateGroceryItem({ id: item.id, bought: !item.bought })
}
/>
)}
</View>
{!item.approved && (
<View>
<View centerH>
<View height={1} backgroundColor="#e7e7e7" width={"90%"} />
<View height={0.7} backgroundColor="#e7e7e7" width={"98%"} />
</View>
<View paddingL-10 paddingV-15 flexS row centerV>
<View
<View paddingL-0 paddingT-12 flexS row centerV>
<ImageBackground
style={{
width: 25,
width: 22.36,
aspectRatio: 1,
borderRadius: 50,
backgroundColor: "red",
marginHorizontal: 10,
marginRight: 10,
overflow: 'hidden'
}}
></View>
<Text color="#858585" text70>Requested by Austin</Text>
source={require('../../../assets/images/child-picture.png')}
/>
<Text color="#858585" style={styles.authorTxt}>
Requested by {itemCreator?.firstName}
</Text>
</View>
</View>
)}
@ -161,12 +153,19 @@ const GroceryItem = ({
};
const styles = StyleSheet.create({
checkbox:{
authorTxt: { fontFamily: "Manrope_500Medium", fontSize: 12 },
checkbox: {
borderRadius: 50,
borderWidth: 1,
borderWidth: 0.7,
color: "#bfbfbf",
borderColor: "#bfbfbf",
}
})
width: 24.64,
aspectRatio: 1,
},
title: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
});
export default GroceryItem;

View File

@ -1,18 +1,18 @@
import { FlatList } from "react-native";
import { FlatList, StyleSheet } from "react-native";
import React, { useEffect, useState } from "react";
import { View, Text, ListItem, Button, TextField } from "react-native-ui-lib";
import { Button, Text, TouchableOpacity, View } from "react-native-ui-lib";
import GroceryItem from "./GroceryItem";
import {
IGrocery,
GroceryCategory,
GroceryFrequency,
useGroceryContext,
} from "@/contexts/GroceryContext";
import HeaderTemplate from "@/components/shared/HeaderTemplate";
import CategoryDropdown from "./CategoryDropdown";
import { MaterialIcons } from "@expo/vector-icons";
import { AntDesign, MaterialIcons } from "@expo/vector-icons";
import EditGroceryItem from "./EditGroceryItem";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { IGrocery } from "@/hooks/firebase/types/groceryData";
import AddPersonIcon from "@/assets/svgs/AddPersonIcon";
const GroceryList = () => {
const {
@ -24,10 +24,10 @@ const GroceryList = () => {
} = useGroceryContext();
const { profileData } = useAuthContext();
const [approvedGroceries, setapprovedGroceries] = useState<IGrocery[]>(
groceries.filter((item) => item.approved === true)
groceries?.filter((item) => item.approved === true)
);
const [pendingGroceries, setPendingGroceries] = useState<IGrocery[]>(
groceries.filter((item) => item.approved !== true)
groceries?.filter((item) => item.approved !== true)
);
const [category, setCategory] = useState<GroceryCategory>(
GroceryCategory.Bakery
@ -35,8 +35,11 @@ const GroceryList = () => {
const [title, setTitle] = useState<string>("");
const [submit, setSubmitted] = useState<boolean>(false);
const [pendingVisible, setPendingVisible] = useState<boolean>(true);
const [approvedVisible, setApprovedVisible] = useState<boolean>(true);
// Group approved groceries by category
const approvedGroceriesByCategory = approvedGroceries.reduce(
const approvedGroceriesByCategory = approvedGroceries?.reduce(
(groups: any, item: IGrocery) => {
const category = item.category || "Uncategorized";
if (!groups[category]) {
@ -52,10 +55,10 @@ const GroceryList = () => {
if (submit) {
if (title?.length > 2 && title?.length <= 25) {
addGrocery({
id: 0,
id: "",
title: title,
category: category,
approved: profileData?.userType === ProfileType.PARENT ? true : false,
approved: profileData?.userType === ProfileType.PARENT,
recurring: false,
frequency: GroceryFrequency.Never,
bought: false,
@ -73,8 +76,8 @@ const GroceryList = () => {
}, [category]);
useEffect(() => {
setapprovedGroceries(groceries.filter((item) => item.approved === true));
setPendingGroceries(groceries.filter((item) => item.approved !== true));
setapprovedGroceries(groceries?.filter((item) => item.approved === true));
setPendingGroceries(groceries?.filter((item) => item.approved !== true));
}, [groceries]);
return (
@ -83,20 +86,23 @@ const GroceryList = () => {
message={"Welcome to your grocery list"}
isWelcome={false}
>
<View row spread gap-5>
<View row centerV>
<View
backgroundColor="#e2eed8"
padding-8
paddingH-15
paddingV-8
marginR-5
centerV
style={{ borderRadius: 50 }}
>
<Text text70BL color="#46a80a">
{approvedGroceries.length} list{" "}
{approvedGroceries.length === 1 ? (
<Text text70BL color="#46a80a">
<Text text70BL color="#46a80a" style={styles.counterText}>
{approvedGroceries?.length} list{" "}
{approvedGroceries?.length === 1 ? (
<Text text70BL color="#46a80a" style={styles.counterText}>
item
</Text>
) : (
<Text text70BL color="#46a80a">
<Text text70BL color="#46a80a" style={styles.counterText}>
items
</Text>
)}
@ -105,65 +111,118 @@ const GroceryList = () => {
<View
backgroundColor="#faead2"
padding-8
paddingH-12
marginR-15
style={{ borderRadius: 50 }}
>
<Text text70BL color="#e28800">
{pendingGroceries.length} pending
<Text text70 style={styles.counterText} color="#e28800">
{pendingGroceries?.length} pending
</Text>
</View>
<Button
backgroundColor="transparent"
paddingH-10
iconSource={() => (
<MaterialIcons name="person-add-alt" size={24} color="gray" />
)}
/>
<TouchableOpacity>
<AddPersonIcon width={24}/>
</TouchableOpacity>
</View>
</HeaderTemplate>
{/* Pending Approval Section */}
<View row spread marginT-40 marginB-20 centerV>
<Text text70BL>Pending Approval</Text>
<View row spread marginT-40 marginB-10 centerV>
<View row centerV>
<Text style={styles.subHeader}>Pending Approval</Text>
{pendingVisible && (
<AntDesign
name="down"
size={17}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setPendingVisible(false);
}}
/>
)}
{!pendingVisible && (
<AntDesign
name="right"
size={15}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setPendingVisible(true);
}}
/>
)}
</View>
<View
centerV
style={{
aspectRatio: 1,
width: 40,
width: 35,
backgroundColor: "#faead2",
borderRadius: 50,
}}
>
<Text text60L center color="#e28800">
{pendingGroceries.length.toString()}
<Text style={styles.counterNr} center color="#e28800">
{pendingGroceries?.length.toString()}
</Text>
</View>
</View>
{pendingGroceries.length > 0 ? (
<FlatList
data={pendingGroceries}
renderItem={({ item }) => (
<GroceryItem item={item} handleItemApproved={updateGroceryItem} />
{pendingGroceries?.length > 0
? pendingVisible && (
<FlatList
data={pendingGroceries}
renderItem={({ item }) => (
<GroceryItem
item={item}
handleItemApproved={(id, changes) =>
updateGroceryItem({ ...changes, id: id })
}
/>
)}
keyExtractor={(item) => item.id.toString()}
/>
)
: pendingVisible && (
<Text style={styles.noItemTxt}>No items pending approval.</Text>
)}
keyExtractor={(item) => item.id.toString()}
/>
) : (
<Text>No items pending approval.</Text>
)}
{/* Approved Section */}
<View row spread marginT-40 marginB-20 centerV>
<Text text70BL>Shopping List</Text>
<View row spread marginT-40 marginB-0 centerV>
<View row centerV>
<Text style={styles.subHeader}>Shopping List</Text>
{approvedVisible && (
<AntDesign
name="down"
size={17}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setApprovedVisible(false);
}}
/>
)}
{!approvedVisible && (
<AntDesign
name="right"
size={15}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setApprovedVisible(true);
}}
/>
)}
</View>
<View
centerV
style={{
aspectRatio: 1,
width: 40,
width: 35,
backgroundColor: "#e2eed8",
borderRadius: 50,
}}
>
<Text text60L center color="#46a80a">
{approvedGroceries.length.toString()}
<Text style={styles.counterNr} center color="#46a80a">
{approvedGroceries?.length.toString()}
</Text>
</View>
</View>
@ -180,38 +239,60 @@ const GroceryList = () => {
)}
{/* Render Approved Groceries Grouped by Category */}
{approvedGroceries.length > 0 ? (
<FlatList
data={Object.keys(approvedGroceriesByCategory)}
renderItem={({ item: category }) => (
<View key={category}>
{/* Render Category Header */}
<Text
text70M
style={{ marginVertical: 10, paddingHorizontal: 15 }}
color="#666"
>
{category}
</Text>
{/* Render Grocery Items for this Category */}
{approvedGroceriesByCategory[category].map(
(grocery: IGrocery) => (
<GroceryItem
key={grocery.id}
item={grocery}
handleItemApproved={updateGroceryItem}
/>
)
{approvedGroceries?.length > 0
? approvedVisible && (
<FlatList
data={Object.keys(approvedGroceriesByCategory)}
renderItem={({ item: category }) => (
<View key={category}>
{/* Render Category Header */}
<Text text80M style={{ marginTop: 10 }} color="#666">
{category}
</Text>
{/* Render Grocery Items for this Category */}
{approvedGroceriesByCategory[category].map(
(grocery: IGrocery) => (
<GroceryItem
key={grocery.id}
item={grocery}
handleItemApproved={(id, changes) =>
updateGroceryItem({ ...changes, id: id })
}
/>
)
)}
</View>
)}
</View>
keyExtractor={(category) => category}
/>
)
: approvedVisible && (
<Text style={styles.noItemTxt}>No approved items.</Text>
)}
keyExtractor={(category) => category}
/>
) : (
<Text>No approved items.</Text>
)}
</View>
);
};
const styles = StyleSheet.create({
dropIcon: {
marginHorizontal: 10,
},
noItemTxt: {
fontFamily: "Manrope_400Regular",
fontSize: 14,
},
counterText: {
fontSize: 14,
fontFamily: "PlusJakartaSans_600SemiBold",
},
subHeader: {
fontSize: 15,
fontFamily: "Manrope_700Bold",
},
counterNr: {
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 14
}
});
export default GroceryList;

View File

@ -1,20 +1,36 @@
import { Text, ScrollView } from "react-native";
import { View } from "react-native-ui-lib";
import React from "react";
import React, { useEffect, useRef } from "react";
import AddGroceryItem from "./AddGroceryItem";
import GroceryList from "./GroceryList";
import { useGroceryContext } from "@/contexts/GroceryContext";
const GroceryWrapper = () => {
const { isAddingGrocery } = useGroceryContext();
const scrollViewRef = useRef<ScrollView>(null); // Reference to the ScrollView
useEffect(() => {
if (isAddingGrocery && scrollViewRef.current) {
scrollViewRef.current.scrollTo({
y: 400, // Adjust this value to scroll a bit down (100 is an example)
animated: true,
});
}
}, [isAddingGrocery]);
return (
<View height={"100%"}>
<View height={'90%'}>
<ScrollView>
<GroceryList />
<View height={"100%"} paddingT-15 paddingH-15>
<View height={"100%"}>
<ScrollView
ref={scrollViewRef} // Assign the ref to the ScrollView
automaticallyAdjustKeyboardInsets={true}
>
<View marginB-70>
<GroceryList />
</View>
</ScrollView>
</View>
{!isAddingGrocery && <AddGroceryItem />}
</View>
</View>
);
};

View File

@ -31,7 +31,7 @@ const SignUpPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
return (
<View padding-10 height={"100%"} flexG>
<Text text30 center>
Get started with Kali
Get started with Cally
</Text>
<Text center>Please enter your details.</Text>
<TextField

View File

@ -28,21 +28,28 @@ const OnboardingFlow = () => {
width={10}
/>
),
title: <Text text30>Welcome to Kali</Text>,
title: <Text text30>Welcome to Cally</Text>,
subtitle: (
<View paddingB-250 marginH-20 spread>
<Text text50R>Lightening Mental Loads, One Family at a Time</Text>
<Button
label="Continue"
style={{ backgroundColor: "#fd1775" }}
onPress={() => onboardingRef.current.goToPage(1, true)}
onPress={() => onboardingRef?.current?.goToPage(1, true)}
/>
</View>
),
},
{
backgroundColor: "#f9f8f7",
title: <Text>Get started with Kali</Text>,
title: <Text>Get started with Cally</Text>,
image: (
<Image
source={require("../../../assets/images/splash-clock.png")}
height={10}
width={10}
/>
),
subtitle: (
<View
style={{

View File

@ -1,350 +1,393 @@
import {AntDesign, Ionicons} from "@expo/vector-icons";
import React, {useEffect, useState} from "react";
import {Button, Checkbox, Text, View} from "react-native-ui-lib";
import {ScrollView, StyleSheet} from "react-native";
import {colorMap} from "@/contexts/SettingsContext";
import {TouchableOpacity} from "react-native-gesture-handler";
import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils";
import {fetchMicrosoftCalendarEvents} from "@/calendar-integration/microsoft-calendar-utils";
import {useCreateEventFromProvider} from "@/hooks/firebase/useCreateEvent";
import {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
import { AntDesign, Ionicons } from "@expo/vector-icons";
import React, { useCallback, useEffect, useState } from "react";
import { Button, Checkbox, Text, View } from "react-native-ui-lib";
import { ScrollView, StyleSheet } from "react-native";
import { colorMap } from "@/contexts/SettingsContext";
import { TouchableOpacity } from "react-native-gesture-handler";
import { fetchGoogleCalendarEvents } from "@/calendar-integration/google-calendar-utils";
import { fetchMicrosoftCalendarEvents } from "@/calendar-integration/microsoft-calendar-utils";
import { useCreateEventFromProvider } from "@/hooks/firebase/useCreateEvent";
import { useAuthContext } from "@/contexts/AuthContext";
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
import debounce from "debounce";
import AppleIcon from "@/assets/svgs/AppleIcon";
import GoogleIcon from "@/assets/svgs/GoogleIcon";
import OutlookIcon from "@/assets/svgs/OutlookIcon";
import * as AuthSession from "expo-auth-session";
import * as Google from "expo-auth-session/providers/google";
import * as WebBrowser from "expo-web-browser";
const microsoftConfig = {
clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Replace with your Microsoft client ID
redirectUri: AuthSession.makeRedirectUri({path: "settings"}), // Generate redirect URI automatically for Expo
scopes: [
"openid",
"profile",
"email",
"offline_access",
"Calendars.ReadWrite", // Scope for reading calendar events
],
authorizationEndpoint:
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token"
const googleConfig = {
androidClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
iosClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
webClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
scopes: [
"email",
"profile",
"https://www.googleapis.com/auth/calendar.events.owned",
],
};
const googleConfig = {
androidClientId: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
iosClientId: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
webClientId: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
scopes: ["email", "profile", "https://www.googleapis.com/auth/calendar.events.owned"]
const microsoftConfig = {
clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Replace with your Microsoft client ID
redirectUri: AuthSession.makeRedirectUri({ path: "settings" }), // Generate redirect URI automatically for Expo
scopes: [
"openid",
"profile",
"email",
"offline_access",
"Calendars.ReadWrite", // Scope for reading calendar events
],
authorizationEndpoint:
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
};
const CalendarSettingsPage = (props: {
setSelectedPage: (page: number) => void;
setSelectedPage: (page: number) => void;
}) => {
const [selectedColor, setSelectedColor] = useState<string>(colorMap.pink);
const [startDate, setStartDate] = useState<boolean>(true);
const {profileData} = useAuthContext();
const [startDate, setStartDate] = useState<boolean>(false);
const { profileData } = useAuthContext();
const {mutateAsync: createEventFromProvider} = useCreateEventFromProvider();
const {mutateAsync: updateUserData} = useUpdateUserData();
const [selectedColor, setSelectedColor] = useState<string>(
profileData?.eventColor ?? colorMap.pink
);
const [previousSelectedColor, setPreviousSelectedColor] = useState<string>(
profileData?.eventColor ?? colorMap.pink
);
WebBrowser.maybeCompleteAuthSession()
const [request, response, promptAsync] = Google.useAuthRequest(googleConfig);
const { mutateAsync: createEventFromProvider } = useCreateEventFromProvider();
const { mutateAsync: updateUserData } = useUpdateUserData();
const signInWithGoogle = async () => {
try {
// Attempt to retrieve user information from AsyncStorage
if (response?.type === 'success') {
console.log(response.authentication)
await updateUserData({newUserData: {googleToken: response.authentication?.accessToken}})
}
} catch (error) {
// Handle any errors that occur during AsyncStorage retrieval or other operations
console.error("Error retrieving user data from AsyncStorage:", error);
}
};
WebBrowser.maybeCompleteAuthSession();
const [request, response, promptAsync] = Google.useAuthRequest(googleConfig);
useEffect(() => {
signInWithGoogle();
}, [response]);
useEffect(() => {
signInWithGoogle();
}, [response]);
const fetchAndSaveGoogleEvents = () => {
const timeMin = new Date(new Date().setHours(0, 0, 0, 0));
const timeMax = new Date(
new Date(new Date().setHours(0, 0, 0, 0)).setDate(timeMin.getDate() + 30)
);
fetchGoogleCalendarEvents(
profileData?.googleToken,
timeMin.toISOString().slice(0, -5) + "Z",
timeMax.toISOString().slice(0, -5) + "Z",
).then((response) => {
response?.forEach((item) => saveData(item));
});
};
async function saveData(item: any) {
await createEventFromProvider(item);
}
const fetchAndSaveMicrosoftEvents = () => {
const startDateTime = new Date(new Date().setHours(0, 0, 0, 0));
const endDateTime = new Date(
new Date(new Date().setHours(0, 0, 0, 0)).setDate(
startDateTime.getDate() + 30
)
);
fetchMicrosoftCalendarEvents(
profileData?.microsoftToken,
startDateTime.toISOString().slice(0, -5) + "Z",
endDateTime.toISOString().slice(0, -5) + "Z"
).then((response) => {
console.log(response)
response?.forEach((item) => saveData(item));
});
};
const handleMicrosoftSignIn = async () => {
try {
console.log("Starting Microsoft sign-in...");
const authRequest = new AuthSession.AuthRequest({
clientId: microsoftConfig.clientId,
scopes: microsoftConfig.scopes,
redirectUri: microsoftConfig.redirectUri,
responseType: AuthSession.ResponseType.Code,
usePKCE: true, // Enable PKCE
});
console.log("Auth request created:", authRequest);
const authResult = await authRequest.promptAsync({
authorizationEndpoint: microsoftConfig.authorizationEndpoint,
});
console.log("Auth result:", authResult);
if (authResult.type === "success" && authResult.params?.code) {
const code = authResult.params.code;
console.log("Authorization code received:", code);
// Exchange authorization code for tokens
const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `client_id=${microsoftConfig.clientId}&redirect_uri=${encodeURIComponent(
microsoftConfig.redirectUri
)}&grant_type=authorization_code&code=${code}&code_verifier=${
authRequest.codeVerifier
}&scope=${encodeURIComponent("https://graph.microsoft.com/Calendars.ReadWrite offline_access")}`,
});
console.log("Token response status:", tokenResponse.status);
if (!tokenResponse.ok) {
console.error("Token exchange failed:", await tokenResponse.text());
return;
}
const tokenData = await tokenResponse.json();
console.log("Token data received:", tokenData);
if (tokenData?.id_token) {
console.log("ID token received, updating user data...");
await updateUserData({newUserData: {microsoftToken: tokenData.access_token}});
console.log("User data updated successfully.");
}
} else {
console.warn("Authentication was not successful:", authResult);
}
} catch (error) {
console.error("Error during Microsoft sign-in:", error);
}
};
return (
<ScrollView>
<View marginH-30>
<TouchableOpacity onPress={() => props.setSelectedPage(0)}>
<View row marginT-20 marginB-35 centerV>
<Ionicons name="chevron-back" size={22} color="#979797"/>
<Text text70 color="#979797">
Return to main settings
</Text>
</View>
</TouchableOpacity>
<Text text60R>Calendar settings</Text>
<View style={styles.card}>
<Text text70 marginB-14>
Event Color Preference
</Text>
<View row spread>
<TouchableOpacity onPress={() => setSelectedColor(colorMap.pink)}>
<View style={styles.colorBox} backgroundColor={colorMap.pink}>
{selectedColor == colorMap.pink && (
<AntDesign name="check" size={30} color="white"/>
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => setSelectedColor(colorMap.orange)}>
<View style={styles.colorBox} backgroundColor={colorMap.orange}>
{selectedColor == colorMap.orange && (
<AntDesign name="check" size={30} color="white"/>
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => setSelectedColor(colorMap.green)}>
<View style={styles.colorBox} backgroundColor={colorMap.green}>
{selectedColor == colorMap.green && (
<AntDesign name="check" size={30} color="white"/>
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => setSelectedColor(colorMap.teal)}>
<View style={styles.colorBox} backgroundColor={colorMap.teal}>
{selectedColor == colorMap.teal && (
<AntDesign name="check" size={30} color="white"/>
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => setSelectedColor(colorMap.purple)}>
<View style={styles.colorBox} backgroundColor={colorMap.purple}>
{selectedColor == colorMap.purple && (
<AntDesign name="check" size={30} color="white"/>
)}
</View>
</TouchableOpacity>
</View>
</View>
<View style={styles.card}>
<Text text70>Weekly Start Date</Text>
<View row marginV-5 marginT-20>
<Checkbox
value={startDate}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => setStartDate(true)}
/>
<View row marginL-8>
<Text text70>Sundays</Text>
<Text text70 color="gray">
{" "}
(default)
</Text>
</View>
</View>
<View row marginV-5>
<Checkbox
value={!startDate}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => setStartDate(false)}
/>
<Text text70 marginL-8>
Mondays
</Text>
</View>
</View>
<View style={styles.card}>
<Text text70>Add Calendar</Text>
<View style={{marginTop: 20}}>
<Button
label={"Connect Google"}
iconSource={() => (
<View
backgroundColor="#ededed"
width={40}
height={40}
style={{borderRadius: 50}}
marginR-10
centerV
centerH
>
<Ionicons name="logo-google" size={22} color="#979797"/>
</View>
)}
backgroundColor="white"
color="#464039"
borderRadius={15}
onPress={() => promptAsync()}
/>
<Button
label={"Connect Microsoft"}
iconSource={() => (
<View
backgroundColor="#ededed"
width={40}
height={40}
style={{borderRadius: 50}}
marginR-10
centerV
centerH
>
<Ionicons name="logo-microsoft" size={22} color="#979797"/>
</View>
)}
backgroundColor="white"
color="#464039"
borderRadius={15}
onPress={handleMicrosoftSignIn}
/>
</View>
</View>
<View style={styles.card}>
<Text text70>Calendars</Text>
<View style={{marginTop: 20}}>
{profileData?.googleToken !== undefined && profileData.googleToken !== '' && <Button
label={"Sync Google"}
iconSource={() => (
<View
backgroundColor="#ededed"
width={40}
height={40}
style={{borderRadius: 50}}
marginR-10
centerV
centerH
>
<Ionicons name="logo-google" size={22} color="#979797"/>
</View>
)}
backgroundColor="white"
color="#464039"
borderRadius={15}
onPress={fetchAndSaveGoogleEvents}
/>}
{profileData?.microsoftToken !== undefined && profileData.microsoftToken !== '' &&<Button
label={"Sync Outlook"}
iconSource={() => (
<View
backgroundColor="#ededed"
width={40}
height={40}
style={{borderRadius: 50}}
marginR-10
centerV
centerH
>
<Ionicons name="logo-microsoft" size={22} color="#979797"/>
</View>
)}
backgroundColor="white"
color="#464039"
borderRadius={15}
onPress={fetchAndSaveMicrosoftEvents}
/>}
</View>
</View>
</View>
</ScrollView>
const fetchAndSaveGoogleEvents = () => {
console.log("fetch");
const timeMin = new Date(new Date().setHours(0, 0, 0, 0));
const timeMax = new Date(
new Date(new Date().setHours(0, 0, 0, 0)).setDate(timeMin.getDate() + 30)
);
fetchGoogleCalendarEvents(
profileData?.googleToken,
timeMin.toISOString().slice(0, -5) + "Z",
timeMax.toISOString().slice(0, -5) + "Z"
).then((response) => {
response?.forEach((item) => saveData(item));
});
};
async function saveData(item: any) {
await createEventFromProvider(item);
}
const fetchAndSaveMicrosoftEvents = () => {
const startDateTime = new Date(new Date().setHours(0, 0, 0, 0));
const endDateTime = new Date(
new Date(new Date().setHours(0, 0, 0, 0)).setDate(
startDateTime.getDate() + 30
)
);
fetchMicrosoftCalendarEvents(
profileData?.microsoftToken,
startDateTime.toISOString().slice(0, -5) + "Z",
endDateTime.toISOString().slice(0, -5) + "Z"
).then((response) => {
console.log(response);
response?.forEach((item) => saveData(item));
});
};
const signInWithGoogle = async () => {
try {
// Attempt to retrieve user information from AsyncStorage
if (response?.type === "success") {
console.log(response.authentication);
await updateUserData({
newUserData: { googleToken: response.authentication?.accessToken },
});
}
} catch (error) {
// Handle any errors that occur during AsyncStorage retrieval or other operations
console.error("Error retrieving user data from AsyncStorage:", error);
}
};
const handleMicrosoftSignIn = async () => {
try {
console.log("Starting Microsoft sign-in...");
const authRequest = new AuthSession.AuthRequest({
clientId: microsoftConfig.clientId,
scopes: microsoftConfig.scopes,
redirectUri: microsoftConfig.redirectUri,
responseType: AuthSession.ResponseType.Code,
usePKCE: true, // Enable PKCE
});
console.log("Auth request created:", authRequest);
const authResult = await authRequest.promptAsync({
authorizationEndpoint: microsoftConfig.authorizationEndpoint,
});
console.log("Auth result:", authResult);
if (authResult.type === "success" && authResult.params?.code) {
const code = authResult.params.code;
console.log("Authorization code received:", code);
// Exchange authorization code for tokens
const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `client_id=${
microsoftConfig.clientId
}&redirect_uri=${encodeURIComponent(
microsoftConfig.redirectUri
)}&grant_type=authorization_code&code=${code}&code_verifier=${
authRequest.codeVerifier
}&scope=${encodeURIComponent(
"https://graph.microsoft.com/Calendars.ReadWrite offline_access"
)}`,
});
console.log("Token response status:", tokenResponse.status);
if (!tokenResponse.ok) {
console.error("Token exchange failed:", await tokenResponse.text());
return;
}
const tokenData = await tokenResponse.json();
console.log("Token data received:", tokenData);
if (tokenData?.id_token) {
console.log("ID token received, updating user data...");
await updateUserData({
newUserData: { microsoftToken: tokenData.access_token },
});
console.log("User data updated successfully.");
}
} else {
console.warn("Authentication was not successful:", authResult);
}
} catch (error) {
console.error("Error during Microsoft sign-in:", error);
}
};
const debouncedUpdateUserData = useCallback(
debounce(async (color: string) => {
try {
await updateUserData({
newUserData: {
eventColor: color,
},
});
} catch (error) {
console.error("Failed to update color:", error);
setSelectedColor(previousSelectedColor);
}
}, 500),
[]
);
const handleChangeColor = (color: string) => {
setPreviousSelectedColor(selectedColor);
setSelectedColor(color);
debouncedUpdateUserData(color);
};
return (
<ScrollView>
<View marginH-30>
<TouchableOpacity onPress={() => props.setSelectedPage(0)}>
<View row marginT-20 marginB-35 centerV>
<Ionicons
name="chevron-back"
size={14}
color="#979797"
style={{ paddingBottom: 3 }}
/>
<Text
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
color="#979797"
>
Return to main settings
</Text>
</View>
</TouchableOpacity>
<Text style={styles.subTitle}>Calendar settings</Text>
<View style={styles.card}>
<Text style={styles.cardTitle} marginB-14>
Event Color Preference
</Text>
<View row spread>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.pink)}>
<View style={styles.colorBox} backgroundColor={colorMap.pink}>
{selectedColor == colorMap.pink && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.orange)}
>
<View style={styles.colorBox} backgroundColor={colorMap.orange}>
{selectedColor == colorMap.orange && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.green)}>
<View style={styles.colorBox} backgroundColor={colorMap.green}>
{selectedColor == colorMap.green && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.teal)}>
<View style={styles.colorBox} backgroundColor={colorMap.teal}>
{selectedColor == colorMap.teal && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.purple)}
>
<View style={styles.colorBox} backgroundColor={colorMap.purple}>
{selectedColor == colorMap.purple && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
</View>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>Weekly Start Date</Text>
<View row marginV-5 marginT-20>
<Checkbox
value={startDate}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => setStartDate(true)}
/>
<View row marginL-8>
<Text text70>Sundays</Text>
<Text text70 color="gray">
{" "}
(default)
</Text>
</View>
</View>
<View row marginV-5>
<Checkbox
value={!startDate}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => setStartDate(false)}
/>
<Text text70 marginL-8>
Mondays
</Text>
</View>
</View>
<Text style={styles.subTitle} marginT-30 marginB-25>
Add Calendar
</Text>
<Button
onPress={() => promptAsync()}
label="Connect Google"
labelStyle={styles.addCalLbl}
iconSource={() => (
<View marginR-15>
<GoogleIcon />
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<Button
label="Connect Apple"
labelStyle={styles.addCalLbl}
iconSource={() => (
<View marginR-15>
<AppleIcon />
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<Button
onPress={handleMicrosoftSignIn}
label="Connect Outlook"
labelStyle={styles.addCalLbl}
iconSource={() => (
<View marginR-15>
<OutlookIcon />
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<Text style={styles.subTitle} marginT-30 marginB-20>
Connected Calendars
</Text>
<View style={styles.card}>
<View style={{ marginTop: 20 }}>
<Button
onPress={fetchAndSaveGoogleEvents}
label="Sync Google"
labelStyle={styles.addCalLbl}
iconSource={() => (
<View marginR-15>
<GoogleIcon />
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<Button
onPress={fetchAndSaveMicrosoftEvents}
label="Sync Outlook"
labelStyle={styles.addCalLbl}
iconSource={() => (
<View marginR-15>
<OutlookIcon />
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
</View>
</View>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
addCalBtn: {
addCalBtn: {
backgroundColor: "#ffffff",
marginBottom: 15,
justifyContent: "flex-start",
@ -361,18 +404,30 @@ const styles = StyleSheet.create({
padding: 20,
paddingBottom: 30,
marginTop: 20,
borderRadius: 20,
borderRadius: 12,
},
colorBox: {
aspectRatio: 1,
justifyContent: "center",
alignItems: "center",
width: 50,
width: 51,
borderRadius: 12,
},
checkbox: {
borderRadius: 50,
},
addCalLbl: {
fontSize: 16,
fontFamily: "PlusJakartaSan_500Medium",
},
subTitle: {
fontFamily: "Manrope_600SemiBold",
fontSize: 18,
},
cardTitle: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
});
export default CalendarSettingsPage;

View File

@ -10,20 +10,28 @@ const ChoreRewardSettings = (props: {
}) => {
return (
<ToDosContextProvider>
<View marginT-10>
<View marginT-10 marginH-20>
<ScrollView>
<TouchableOpacity onPress={() => props.setSelectedPage(0)}>
<View row marginT-20 marginL-20 marginB-35 centerV>
<Ionicons name="chevron-back" size={22} color="#979797" />
<Text text70 color="#979797">
<View row marginT-20 marginB-35 centerV>
<Ionicons
name="chevron-back"
size={14}
color="#979797"
style={{ paddingBottom: 3 }}
/>
<Text
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
color="#979797"
>
Return to main settings
</Text>
</View>
</TouchableOpacity>
<Text text60R marginL-30 marginB-20>
<Text text60R marginB-20>
Chore Reward Settings
</Text>
<ToDosList />
<ToDosList isSettings />
</ScrollView>
</View>
</ToDosContextProvider>

View File

@ -6,15 +6,10 @@ import CalendarSettingsPage from "./CalendarSettingsPage";
import ChoreRewardSettings from "./ChoreRewardSettings";
import UserSettings from "./UserSettings";
import { AuthContextProvider } from "@/contexts/AuthContext";
import ProfileIcon from "@/assets/svgs/ProfileIcon";
import CalendarIcon from "@/assets/svgs/CalendarIcon";
import PrivacyPolicyIcon from "@/assets/svgs/PrivacyPolicyIcon";
const styles = StyleSheet.create({
mainBtn: {
width: "100%",
justifyContent: "flex-start",
marginBottom: 20,
height: 60,
},
});
const pageIndex = {
main: 0,
user: 1,
@ -31,30 +26,22 @@ const SettingsPage = () => {
<Button
backgroundColor="white"
style={styles.mainBtn}
label="Manage my profile"
label="Manage My Profile"
labelStyle={styles.label}
color="#07b8c7"
iconSource={() => (
<Ionicons
name="person-circle-sharp"
size={24}
color="#07b8c7"
style={{ marginRight: 10 }}
/>
<ProfileIcon style={{marginRight: 10}} color="#07b9c8" />
)}
onPress={() => setSelectedPage(pageIndex.user)}
/>
<Button
backgroundColor="white"
style={styles.mainBtn}
label="Calendar settings"
label="Calendar Settings"
labelStyle={styles.label}
color="#fd1775"
iconSource={() => (
<Ionicons
name="home-outline"
size={24}
color="#fd1775"
style={{ marginRight: 10 }}
/>
<CalendarIcon style={{marginRight: 10}}/>
)}
onPress={() => {
setSelectedPage(pageIndex.calendar);
@ -63,7 +50,8 @@ const SettingsPage = () => {
<Button
backgroundColor="white"
style={styles.mainBtn}
label="To Do reward settings"
label="To-Do Reward Settings"
labelStyle={styles.label}
color="#ff9900"
iconSource={() => (
<Octicons
@ -78,14 +66,10 @@ const SettingsPage = () => {
<Button
backgroundColor="white"
style={styles.mainBtn}
label="Kaly privacy policy"
label="Cally Privacy Policy"
labelStyle={styles.label}
iconSource={() => (
<Entypo
name="text-document"
size={24}
color="#6c645b"
style={{ marginRight: 10 }}
/>
<PrivacyPolicyIcon style={{marginRight: 10}}/>
)}
color="#6c645b"
/>
@ -105,3 +89,17 @@ const SettingsPage = () => {
};
export default SettingsPage;
const styles = StyleSheet.create({
mainBtn: {
width: "100%",
justifyContent: "flex-start",
marginBottom: 20,
height: 60,
},
label:{
fontFamily: "Poppins_400Regular",
fontSize: 14.71,
textAlignVertical: 'center'
}
});

View File

@ -11,9 +11,12 @@ const UserSettings = (props: { setSelectedPage: (page: number) => void }) => {
<View flexG>
<ScrollView style={{ paddingBottom: 20, minHeight: "100%" }}>
<TouchableOpacity onPress={() => props.setSelectedPage(0)}>
<View row marginT-20 marginL-20 marginB-35 centerV>
<Ionicons name="chevron-back" size={22} color="#979797" />
<Text text70 color="#979797">
<View row marginT-20 marginB-35 centerV>
<Ionicons name="chevron-back" size={14} color="#979797" style={{paddingBottom: 3}} />
<Text
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
color="#979797"
>
Return to main settings
</Text>
</View>
@ -30,7 +33,10 @@ const UserSettings = (props: { setSelectedPage: (page: number) => void }) => {
style={selectedView == true ? styles.btnSelected : styles.btnNot}
>
<View>
<Text text70 color={selectedView ? "white" : "black"}>
<Text
style={styles.btnTxt}
color={selectedView ? "white" : "black"}
>
My Profile
</Text>
</View>
@ -42,7 +48,10 @@ const UserSettings = (props: { setSelectedPage: (page: number) => void }) => {
style={selectedView == false ? styles.btnSelected : styles.btnNot}
>
<View>
<Text text70 color={!selectedView ? "white" : "black"}>
<Text
style={styles.btnTxt}
color={!selectedView ? "white" : "black"}
>
My Group
</Text>
</View>
@ -55,7 +64,7 @@ const UserSettings = (props: { setSelectedPage: (page: number) => void }) => {
{!selectedView && (
<View>
<Text>Nigga</Text>
<Text>selview</Text>
</View>
)}
</View>
@ -75,11 +84,16 @@ const styles = StyleSheet.create({
width: "50%",
borderRadius: 50,
},
btnTxt: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
btnNot: {
height: "100%",
width: "50%",
borderRadius: 50,
},
title: { fontFamily: "Manrope_600SemiBold", fontSize: 18 },
});
export default UserSettings;

View File

@ -1,322 +1,392 @@
import {
Avatar,
Button,
Card,
Colors,
Dialog,
FloatingButton,
PanningProvider,
Picker,
Text,
TextField,
TouchableOpacity,
View,
Avatar,
Button,
Card,
Colors,
Dialog,
FloatingButton,
PanningProvider,
Picker,
Text,
TextField,
TouchableOpacity,
View,
} from "react-native-ui-lib";
import React, { useState } from "react";
import { ScrollView, StyleSheet } from "react-native";
import { PickerSingleValue } from "react-native-ui-lib/src/components/picker/types";
import { useCreateSubUser } from "@/hooks/firebase/useCreateSubUser";
import { ProfileType } from "@/contexts/AuthContext";
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
import React, {useState} from "react";
import {ScrollView, StyleSheet} from "react-native";
import {PickerSingleValue} from "react-native-ui-lib/src/components/picker/types";
import {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser";
import {ProfileType} from "@/contexts/AuthContext";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import UserMenu from "@/components/pages/settings/user_settings_views/UserMenu";
import {uuidv4} from "@firebase/util";
const MyGroup = () => {
const [showAddUserDialog, setShowAddUserDialog] = useState(false);
const [showNewUserInfoDialog, setShowNewUserInfoDialog] = useState(false);
const [selectedStatus, setSelectedStatus] = useState<
string | PickerSingleValue
>(ProfileType.CHILD);
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [showAddUserDialog, setShowAddUserDialog] = useState(false);
const [showNewUserInfoDialog, setShowNewUserInfoDialog] = useState(false);
const [selectedStatus, setSelectedStatus] = useState<
string | PickerSingleValue
>(ProfileType.CHILD);
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const { mutateAsync: createSubUser, isLoading, isError } = useCreateSubUser();
const { data: familyMembers } = useGetFamilyMembers(true);
const [showQRCodeDialog, setShowQRCodeDialog] = useState("");
const parents =
familyMembers?.filter((x) => x.userType === ProfileType.PARENT) ?? [];
const children =
familyMembers?.filter((x) => x.userType === ProfileType.CHILD) ?? [];
const caregivers =
familyMembers?.filter((x) => x.userType === ProfileType.CAREGIVER) ?? [];
const {mutateAsync: createSubUser, isLoading, isError} = useCreateSubUser();
const {data: familyMembers} = useGetFamilyMembers(true);
const handleCreateSubUser = async () => {
if (!firstName || !lastName || !email) {
console.error("All fields are required");
return;
}
const parents =
familyMembers?.filter((x) => x.userType === ProfileType.PARENT) ?? [];
const children =
familyMembers?.filter((x) => x.userType === ProfileType.CHILD) ?? [];
const caregivers =
familyMembers?.filter((x) => x.userType === ProfileType.CAREGIVER) ?? [];
const familyDevices =
familyMembers?.filter((x) => x.userType === ProfileType.FAMILY_DEVICE) ?? [];
if (!email.includes("@")) {
console.error("Invalid email address");
return;
}
const handleCreateSubUser = async () => {
if (!firstName || (selectedStatus !== ProfileType.FAMILY_DEVICE && !lastName)) {
console.error("First name and last name are required");
return;
}
await createSubUser({
firstName,
lastName,
email,
password: email,
userType: selectedStatus as ProfileType,
});
if (selectedStatus !== ProfileType.FAMILY_DEVICE && !email) {
console.error("Email is required for non-family device users");
return;
}
if (!isError) {
setShowNewUserInfoDialog(false);
}
};
if (email && !email.includes("@")) {
console.error("Invalid email address");
return;
}
console.log(familyMembers);
const res = await createSubUser({
firstName,
lastName: selectedStatus === ProfileType.FAMILY_DEVICE ? "" : lastName,
email: email || `placeholder_${uuidv4().split("-")[0]}@family.device`,
password: uuidv4(),
userType: selectedStatus as ProfileType,
});
console.log(res)
return (
<View style={{ flex: 1 }}>
<View>
<ScrollView style={styles.card}>
{!parents.length && !children.length && !caregivers.length && (
<Text text70 marginV-10>
{isLoading ? "Loading...." : "No user devices added"}
</Text>
)}
if (!isError) {
setShowNewUserInfoDialog(false);
{(!!parents.length || !!children.length) && (
<>
<Text text70 marginV-10>
Family
</Text>
{[...parents, ...children]?.map((member) => (
<Card
enableShadow={false}
elevation={0}
key={`${member.firstName}_${member.lastName}`}
style={styles.familyCard}
row
centerV
padding-10
>
<Avatar
source={{ uri: "https://via.placeholder.com/60" }}
size={40}
backgroundColor={Colors.grey60}
/>
<View marginL-10>
<Text text70M>
{member.firstName} {member.lastName}
</Text>
<Text text90 grey40>
{member.userType === ProfileType.PARENT
? "Admin (You)"
: "Child"}
</Text>
</View>
if(res?.data?.userId) {
setTimeout(() => {
setShowQRCodeDialog(res.data.userId)
}, 500)
}
}
<View flex-1 />
<UserMenu userId={member?.uid!} />
</Card>
))}
</>
)}
};
{!!caregivers.length && (
<>
<Text text70 marginB-10 marginT-15>
Caregivers
</Text>
{caregivers?.map((member) => (
<Card
enableShadow={false}
elevation={0}
key={`${member.firstName}_${member.lastName}`}
style={styles.familyCard}
row
centerV
padding-10
>
<Avatar
source={{ uri: "https://via.placeholder.com/60" }}
size={40}
backgroundColor={Colors.grey60}
/>
<View marginL-10>
<Text text70M>
{member.firstName} {member.lastName}
</Text>
<Text text90 grey40>
Caregiver
</Text>
</View>
</Card>
))}
</>
)}
</ScrollView>
</View>
return (
<View style={{flex: 1}}>
<View>
<ScrollView style={styles.card}>
{!parents.length && !children.length && !caregivers.length && (
<Text text70 marginV-10>
{isLoading ? "Loading...." : "No user devices added"}
</Text>
)}
<FloatingButton
fullWidth
hideBackgroundOverlay
visible
button={{
label: "+ Add a user device",
onPress: () => setShowAddUserDialog(true),
}}
/>
{(!!parents.length || !!children.length) && (
<>
<Text style={styles.subTit} marginV-10>
Family
</Text>
{[...parents, ...children]?.map((member, index) => (
<Card
enableShadow={false}
elevation={0}
key={`${member.firstName}_${member.lastName}_${index}`}
style={styles.familyCard}
row
centerV
padding-10
>
<Avatar
source={{uri: "https://via.placeholder.com/60"}}
size={40}
backgroundColor={Colors.grey60}
/>
<View marginL-10>
<Text text70M>
{member.firstName} {member.lastName}
</Text>
<Text text90 grey40>
{member.userType === ProfileType.PARENT
? "Admin (You)"
: "Child"}
</Text>
</View>
<Dialog
visible={showAddUserDialog}
onDismiss={() => setShowAddUserDialog(false)}
panDirection={PanningProvider.Directions.DOWN}
>
<Card padding-25 gap-10>
<Text>Add a new user device</Text>
<View flex-1/>
<Button backgroundColor={"#FD1775"}>
<Text white>Show a QR Code</Text>
</Button>
<Button
backgroundColor={"#05A8B6"}
onPress={() => {
setShowAddUserDialog(false);
setTimeout(() => {
setShowNewUserInfoDialog(true);
}, 500);
}}
>
<Text white>Enter email address</Text>
</Button>
<UserMenu setShowQRCodeDialog={(val) => setShowQRCodeDialog("")} showQRCodeDialog={showQRCodeDialog === member?.uid} userId={member?.uid!}/>
</Card>
))}
</>
)}
<TouchableOpacity onPress={() => setShowAddUserDialog(false)} center>
<Text>Return to user settings</Text>
</TouchableOpacity>
</Card>
</Dialog>
{!!caregivers.length && (
<>
<Text text70 marginB-10 marginT-15>
Caregivers
</Text>
{caregivers?.map((member) => (
<Card
enableShadow={false}
elevation={0}
key={`${member.firstName}_${member.lastName}`}
style={styles.familyCard}
row
centerV
padding-10
>
<Avatar
source={{uri: "https://via.placeholder.com/60"}}
size={40}
backgroundColor={Colors.grey60}
/>
<View marginL-10>
<Text text70M>
{member.firstName} {member.lastName}
</Text>
<Text text90 grey40>
Caregiver
</Text>
</View>
<Dialog
panDirection={PanningProvider.Directions.DOWN}
visible={showNewUserInfoDialog}
onDismiss={() => setShowNewUserInfoDialog(false)}
>
<Card padding-25 style={styles.dialogCard}>
<View row spread>
<Text text60M>New User Information</Text>
<TouchableOpacity onPress={() => setShowAddUserDialog(false)}>
<Text>X</Text>
</TouchableOpacity>
</View>
<UserMenu setShowQRCodeDialog={(val) => setShowQRCodeDialog("")} showQRCodeDialog={showQRCodeDialog === member?.uid} userId={member?.uid!}/>
</Card>
))}
</>
)}
<View row centerV gap-20 marginV-20>
<Avatar
imageStyle={{ borderRadius: 10 }}
containerStyle={{ borderRadius: 10 }}
size={60}
backgroundColor={Colors.grey60}
{!!familyDevices.length && (
<>
<Text text70 marginB-10 marginT-15>
Family Devices
</Text>
{familyDevices?.map((member, index) => (
<Card
enableShadow={false}
elevation={0}
key={`${member.firstName}_${member.lastName}_${index}`}
style={styles.familyCard}
row
centerV
padding-10
>
<Avatar
source={{uri: "https://via.placeholder.com/60"}}
size={40}
backgroundColor={Colors.grey60}
/>
<View marginL-10>
<Text text70M>
{member.firstName}
</Text>
<Text text90 grey40>
Family Device
</Text>
</View>
<UserMenu setShowQRCodeDialog={(val) => setShowQRCodeDialog("")} showQRCodeDialog={showQRCodeDialog === member?.uid} userId={member?.uid!}/>
</Card>
))}
</>
)}
</ScrollView>
</View>
<FloatingButton
fullWidth
hideBackgroundOverlay
visible
button={{
label: "+ Add a user device",
onPress: () => setShowAddUserDialog(true),
}}
/>
<TouchableOpacity onPress={() => {}}>
<Text style={{ color: Colors.green10 }}>
Upload User Profile Photo
</Text>
</TouchableOpacity>
</View>
<Text style={styles.label}>Member Status</Text>
<Picker
editable={!isLoading}
value={selectedStatus}
//@ts-ignore
onChange={(item) => setSelectedStatus(item)}
style={styles.picker}
showSearch
floatingPlaceholder
>
<Picker.Item label="Child" value={ProfileType.CHILD} />
<Picker.Item label="Parent" value={ProfileType.PARENT} />
<Picker.Item label="Caregiver" value={ProfileType.CAREGIVER} />
</Picker>
<Dialog
visible={showAddUserDialog}
onDismiss={() => setShowAddUserDialog(false)}
panDirection={PanningProvider.Directions.DOWN}
>
<Card padding-25 gap-10>
<Text>Add a new user device</Text>
<Text style={styles.label}>First Name</Text>
<TextField
editable={!isLoading}
placeholder="First name"
value={firstName}
onChangeText={setFirstName}
style={styles.inputField}
/>
<Button backgroundColor={"#FD1775"}>
<Text white>Show a QR Code</Text>
</Button>
<Button
backgroundColor={"#05A8B6"}
onPress={() => {
setShowAddUserDialog(false);
setTimeout(() => {
setShowNewUserInfoDialog(true);
}, 500);
}}
>
<Text white>Enter email address</Text>
</Button>
<Text style={styles.label}>Last Name</Text>
<TextField
editable={!isLoading}
placeholder="Last name"
value={lastName}
onChangeText={setLastName}
style={styles.inputField}
/>
<TouchableOpacity onPress={() => setShowAddUserDialog(false)} center>
<Text>Return to user settings</Text>
</TouchableOpacity>
</Card>
</Dialog>
<Text style={styles.label}>Email Address</Text>
<TextField
editable={!isLoading}
placeholder="Email address"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
style={styles.inputField}
/>
<Dialog
panDirection={PanningProvider.Directions.DOWN}
visible={showNewUserInfoDialog}
onDismiss={() => setShowNewUserInfoDialog(false)}
>
<Card padding-25 style={styles.dialogCard}>
<View row spread>
<Text text60M>New User Information</Text>
<TouchableOpacity onPress={() => setShowAddUserDialog(false)}>
<Text>X</Text>
</TouchableOpacity>
</View>
<Button
disabled={!firstName || !lastName || !email || isLoading}
label={isLoading ? "Adding..." : "Add group member"}
backgroundColor="#FD1775"
style={{ marginTop: 20 }}
onPress={handleCreateSubUser}
/>
</Card>
</Dialog>
</View>
);
<View row centerV gap-20 marginV-20>
<Avatar
imageStyle={{borderRadius: 10}}
containerStyle={{borderRadius: 10}}
size={60}
backgroundColor={Colors.grey60}
/>
<TouchableOpacity onPress={() => {
}}>
<Text style={{color: Colors.green10}}>
Upload User Profile Photo
</Text>
</TouchableOpacity>
</View>
<Text style={styles.label}>Member Status</Text>
<Picker
editable={!isLoading}
value={selectedStatus}
//@ts-ignore
onChange={(item) => setSelectedStatus(item)}
style={styles.picker}
showSearch
floatingPlaceholder
>
<Picker.Item label="Child" value={ProfileType.CHILD}/>
<Picker.Item label="Parent" value={ProfileType.PARENT}/>
<Picker.Item label="Caregiver" value={ProfileType.CAREGIVER}/>
<Picker.Item label="Family Device" value={ProfileType.FAMILY_DEVICE}/>
</Picker>
<Text style={styles.label}>
{selectedStatus === ProfileType.FAMILY_DEVICE ? "Device Name" : "First Name"}
</Text>
<TextField
editable={!isLoading}
placeholder={selectedStatus === ProfileType.FAMILY_DEVICE ? "Device name" : "First name"}
value={firstName}
onChangeText={setFirstName}
style={styles.inputField}
/>
{selectedStatus !== ProfileType.FAMILY_DEVICE && (
<>
<Text style={styles.label}>Last Name</Text>
<TextField
editable={!isLoading}
placeholder="Last name"
value={lastName}
onChangeText={setLastName}
style={styles.inputField}
/>
</>
)}
{selectedStatus !== ProfileType.FAMILY_DEVICE && (
<>
<Text style={styles.label}>Email Address (Optional)</Text>
<TextField
editable={!isLoading}
placeholder="Email address"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
style={styles.inputField}
/>
</>
)}
<Button
disabled={!firstName || (selectedStatus !== ProfileType.FAMILY_DEVICE && !lastName) || isLoading}
label={isLoading ? "Adding..." : "Add group member"}
backgroundColor="#FD1775"
style={{marginTop: 20}}
onPress={handleCreateSubUser}
/>
</Card>
</Dialog>
</View>
);
};
const styles = StyleSheet.create({
card: {
marginVertical: 15,
backgroundColor: "white",
width: "100%",
borderRadius: 15,
padding: 20,
},
familyCard: {
marginBottom: 10,
borderRadius: 10,
backgroundColor: Colors.white,
width: "100%",
},
inputField: {
borderRadius: 50,
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: Colors.grey80,
marginBottom: 16,
borderColor: Colors.grey50,
borderWidth: 1,
height: 40,
},
picker: {
borderRadius: 50,
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: Colors.grey80,
marginBottom: 16,
borderColor: Colors.grey50,
borderWidth: 1,
marginTop: -20,
height: 40,
},
label: {
marginBottom: 5,
fontSize: 12,
color: Colors.grey40,
},
dialogCard: {
borderRadius: 10,
gap: 10,
},
card: {
marginVertical: 15,
backgroundColor: "white",
width: "100%",
borderRadius: 15,
padding: 20,
},
familyCard: {
marginBottom: 10,
borderRadius: 10,
backgroundColor: Colors.white,
width: "100%",
},
inputField: {
borderRadius: 50,
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: Colors.grey80,
marginBottom: 16,
borderColor: Colors.grey50,
borderWidth: 1,
height: 40,
},
picker: {
borderRadius: 50,
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: Colors.grey80,
marginBottom: 16,
borderColor: Colors.grey50,
borderWidth: 1,
marginTop: -20,
height: 40,
},
label: {
marginBottom: 5,
fontSize: 12,
color: Colors.grey40,
},
dialogCard: {
borderRadius: 10,
gap: 10,
},
subTit: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
});
export default MyGroup;

View File

@ -1,10 +1,10 @@
import { View, Text, TextField } from "react-native-ui-lib";
import { Text, TextField, View } from "react-native-ui-lib";
import React, { useState } from "react";
import { StyleSheet } from "react-native";
import { ImageBackground, StyleSheet } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import { useAuthContext } from "@/contexts/AuthContext";
import { useSettingsContext } from "@/contexts/SettingsContext";
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
const MyProfile = () => {
const { user, profileData } = useAuthContext();
@ -15,18 +15,22 @@ const MyProfile = () => {
const { mutateAsync: updateUserData } = useUpdateUserData();
return (
<ScrollView style={{paddingBottom: 100, flex: 1}}>
<ScrollView style={{ paddingBottom: 100, flex: 1 }}>
<View style={styles.card}>
<Text text70>Your Profile</Text>
<Text style={styles.subTit}>Your Profile</Text>
<View row spread paddingH-15 centerV marginV-15>
<View style={styles.pfp}></View>
<Text text80 color="#50be0c">
<ImageBackground
style={styles.pfp}
source={require("../../../../assets/images/profile-picture.png")}
/>
<Text style={styles.photoSet} color="#50be0c">
Change Photo
</Text>
<Text text80>Remove Photo</Text>
<Text style={styles.photoSet}>Remove Photo</Text>
</View>
<View paddingH-15>
<Text text80 marginT-10 marginB-7 color="#a1a1a1">
<Text text80 marginT-10 marginB-7 style={styles.label}>
First name
</Text>
<TextField
@ -39,7 +43,7 @@ const MyProfile = () => {
await updateUserData({ newUserData: { firstName: value } });
}}
/>
<Text text80 marginT-10 marginB-7 color="#a1a1a1">
<Text text80 marginT-10 marginB-7 style={styles.label}>
Last name
</Text>
<TextField
@ -52,7 +56,7 @@ const MyProfile = () => {
await updateUserData({ newUserData: { lastName: value } });
}}
/>
<Text text80 marginT-10 marginB-7 color="#a1a1a1">
<Text text80 marginT-10 marginB-7 style={styles.label}>
Email address
</Text>
<TextField
@ -65,8 +69,8 @@ const MyProfile = () => {
</View>
<View style={styles.card}>
<Text text70>Settings</Text>
<Text text80 marginT-20 marginB-7 color="#a1a1a1">
<Text style={styles.subTit}>Settings</Text>
<Text text80 marginT-20 marginB-7 style={styles.label}>
Time Zone
</Text>
<TextField text70 placeholder="Time Zone" style={styles.txtBox} />
@ -80,12 +84,13 @@ const styles = StyleSheet.create({
marginVertical: 15,
backgroundColor: "white",
width: "100%",
borderRadius: 15,
padding: 20,
borderRadius: 12,
paddingHorizontal: 20,
paddingVertical: 21,
},
pfp: {
aspectRatio: 1,
width: 60,
width: 65.54,
backgroundColor: "green",
borderRadius: 20,
},
@ -96,7 +101,22 @@ const styles = StyleSheet.create({
borderColor: "#cecece",
padding: 15,
height: 45,
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13
},
subTit: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
label: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 12,
color: "#a1a1a1"
},
photoSet:{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13.07
}
});
export default MyProfile;

View File

@ -3,15 +3,22 @@ import {Button, Card, Colors, Dialog, Hint, ListItem, Text, View} from 'react-na
import QRCode from 'react-native-qrcode-svg';
import {PanningDirectionsEnum} from "react-native-ui-lib/src/components/panningViews/panningProvider";
const UserMenu = ({userId}:{userId: string}) => {
const UserMenu = ({
userId,
showQRCodeDialog,
setShowQRCodeDialog
}: {
userId: string,
showQRCodeDialog: boolean,
setShowQRCodeDialog: (value: boolean) => void
}) => {
const [showHint, setShowHint] = useState(false);
const [showQRCodeDialog, setShowQRCodeDialog] = useState(false);
const handleShowQRCode = () => {
setShowHint(false);
setTimeout(() => {
setShowQRCodeDialog(true);
}, 500)
}, 500);
};
return (
@ -22,9 +29,7 @@ const UserMenu = ({userId}:{userId: string}) => {
color={Colors.white}
customContent={
<View height={18}>
<ListItem
onPress={handleShowQRCode}
>
<ListItem onPress={handleShowQRCode}>
<Text>Show Login QR Code</Text>
</ListItem>
</View>
@ -34,7 +39,7 @@ const UserMenu = ({userId}:{userId: string}) => {
backdropColor="transparent"
>
<View>
<Button link onPress={() => setShowHint(x => !x)}>
<Button link onPress={() => setShowHint(!showHint)}>
<Text>...</Text>
</Button>
</View>
@ -47,7 +52,7 @@ const UserMenu = ({userId}:{userId: string}) => {
>
<Card padding-20 center>
<Text marginB-10>Scan this QR Code to Login:</Text>
<QRCode value={userId} size={150} />
<QRCode value={userId} size={150}/>
<Button
marginT-20
label="Close"

View File

@ -1,34 +1,14 @@
import { StyleSheet } from "react-native";
import React, { useEffect, useState } from "react";
import {
Button,
ButtonSize,
View,
Text,
ActionSheet,
TextField,
Dialog,
Slider,
NumberInput,
NumberInputData,
DateTimePicker,
Switch,
Picker,
} from "react-native-ui-lib";
import { AntDesign, Feather, Ionicons } from "@expo/vector-icons";
import React, { useState } from "react";
import { Button, ButtonSize, Text, View } from "react-native-ui-lib";
import { AntDesign } from "@expo/vector-icons";
import LinearGradient from "react-native-linear-gradient";
import { PanningDirectionsEnum } from "react-native-ui-lib/src/components/panningViews/panningProvider";
import { repeatOptions, useToDosContext } from "@/contexts/ToDosContext";
import { setDate } from "date-fns";
import PointsSlider from "@/components/shared/PointsSlider";
import AddChoreDialog from "./AddChoreDialog";
const AddChore = () => {
const [isVisible, setIsVisible] = useState<boolean>(false);
return (
<LinearGradient
colors={["transparent", "#f9f8f7"]}
@ -44,11 +24,11 @@ const AddChore = () => {
>
<AntDesign name="plus" size={24} color="white" />
<Text white text60R marginL-10>
Add to do
Add To Do
</Text>
</Button>
</View>
<AddChoreDialog isVisible={isVisible} setIsVisible={setIsVisible} />
<AddChoreDialog isVisible={isVisible} setIsVisible={setIsVisible} />
</LinearGradient>
);
};

View File

@ -11,21 +11,38 @@ import {
ButtonSize,
} from "react-native-ui-lib";
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
import { StyleSheet } from "react-native";
import { Dimensions, StyleSheet } from "react-native";
import DropModalIcon from "@/assets/svgs/DropModalIcon";
import { IToDo } from "@/hooks/firebase/types/todoData";
interface IAddChoreDialog {
isVisible: boolean;
setIsVisible: (value: boolean) => void;
selectedTodo?: IToDo;
}
const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
const { addToDo, toDos } = useToDosContext();
const [newTitle, setNewTitle] = useState<string>("");
const [points, setPoints] = useState<number>(10);
const [choreDate, setChoreDate] = useState<Date | null>(new Date());
const [rotate, setRotate] = useState<boolean>(false);
const [repeatType, setRepeatType] = useState<string>("Every week");
const defaultTodo = {
id: "",
title: "",
points: 10,
date: new Date(),
rotate: false,
repeatType: "Every week",
};
const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
const { addToDo, updateToDo } = useToDosContext();
const [todo, setTodo] = useState<IToDo>(
addChoreDialogProps.selectedTodo ?? defaultTodo
);
const { width, height } = Dimensions.get("screen");
const [points, setPoints] = useState<number>(todo.points);
const handleClose = () => {
setTodo(defaultTodo);
addChoreDialogProps.setIsVisible(false);
};
const handleChange = (text: string) => {
const numericValue = parseInt(text, 10);
@ -36,16 +53,17 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
setPoints(0);
}
};
return (
<Dialog
bottom={true}
height={"90%"}
width={width}
panDirection={PanningDirectionsEnum.DOWN}
onDismiss={() => addChoreDialogProps.setIsVisible(false)}
onDismiss={() => handleClose}
containerStyle={{
borderRadius: 10,
backgroundColor: "white",
width: "100%",
alignSelf: "stretch",
padding: 0,
paddingTop: 4,
@ -59,13 +77,13 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
style={styles.topBtn}
label="Cancel"
onPress={() => {
addChoreDialogProps.setIsVisible(false);
handleClose();
}}
/>
<View marginT-12>
<DropModalIcon
onPress={() => {
addChoreDialogProps.setIsVisible(false);
handleClose();
}}
/>
</View>
@ -75,17 +93,16 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
label="Save"
onPress={() => {
try {
addToDo({
id: 0,
title: newTitle,
done: false,
date: choreDate,
points: points,
rotate: rotate,
repeatType: repeatType,
});
addChoreDialogProps.setIsVisible(false);
console.log(toDos);
if (addChoreDialogProps.selectedTodo) {
updateToDo({ ...todo, points: points });
} else {
addToDo({
...todo,
done: false,
points: points,
});
}
handleClose();
} catch (error) {
console.error(error);
}
@ -94,9 +111,9 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
</View>
<TextField
placeholder="Add a To Do"
value={newTitle}
value={todo?.title}
onChangeText={(text) => {
setNewTitle(text);
setTodo((oldValue: IToDo) => ({ ...oldValue, title: text }));
}}
placeholderTextColor="#2d2d30"
text60R
@ -106,15 +123,15 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
<View style={styles.divider} marginT-8 />
<View marginL-30 centerV>
<View row marginB-10>
{choreDate && (
{todo?.date && (
<View row centerV>
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={choreDate}
value={todo.date}
text70
marginL-8
onChange={(date) => {
setChoreDate(date);
setTodo((oldValue: IToDo) => ({ ...oldValue, date: date }));
}}
/>
</View>
@ -125,15 +142,21 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
<Picker
marginL-8
placeholder="Select Repeat Type"
value={repeatType}
value={todo?.repeatType}
onChange={(value) => {
if (value) {
if (value.toString() == "None") {
setChoreDate(null);
setRepeatType("None");
setTodo((oldValue) => ({
...oldValue,
date: null,
repeatType: "None",
}));
} else {
setRepeatType(value.toString());
setChoreDate(new Date());
setTodo((oldValue) => ({
...oldValue,
date: new Date(),
repeatType: value.toString(),
}));
}
}
}}
@ -198,9 +221,11 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
<Text text80>Take Turns</Text>
<Switch
onColor={"#ea156c"}
value={rotate}
value={todo.rotate}
marginL-10
onValueChange={(value) => setRotate(value)}
onValueChange={(value) =>
setTodo((oldValue) => ({ ...oldValue, rotate: value }))
}
/>
</View>
<View style={styles.divider} />

View File

@ -3,6 +3,7 @@ import React from "react";
import { Fontisto } from "@expo/vector-icons";
import { ProgressBar } from "react-native-ui-lib/src/components/progressBar";
import { useToDosContext } from "@/contexts/ToDosContext";
import FireworksOrangeIcon from "@/assets/svgs/FireworksOrangeIcon";
const ProgressCard = ({children}: {children?: React.ReactNode}) => {
const { maxPoints } = useToDosContext();
@ -14,8 +15,8 @@ const ProgressCard = ({children}: {children?: React.ReactNode}) => {
style={{ borderRadius: 22 }}
>
<View row centerV>
<Fontisto name="day-sunny" size={30} color="#ff9900" />
<Text marginL-5 text70>
<FireworksOrangeIcon />
<Text marginL-15 text70 style={{fontFamily: 'Manrope_600SemiBold', fontSize: 14}}>
You have earned XX points this week!{" "}
</Text>
</View>
@ -30,8 +31,8 @@ const ProgressCard = ({children}: {children?: React.ReactNode}) => {
}}
/>
<View row spread>
<Text color={"#868686"}>0</Text>
<Text color={"#868686"}>{maxPoints}</Text>
<Text style={{fontSize: 13, color: '#858585'}}>0</Text>
<Text style={{fontSize: 13, color: '#858585'}}>{maxPoints}</Text>
</View>
<View centerV centerH>
{children}

View File

@ -1,65 +1,157 @@
import { View, Text, Checkbox } from "react-native-ui-lib";
import React from "react";
import { IToDo, useToDosContext } from "@/contexts/ToDosContext";
import {
View,
Text,
Checkbox,
TouchableOpacity,
Dialog,
Button,
ButtonSize,
} from "react-native-ui-lib";
import React, { useState } from "react";
import { useToDosContext } from "@/contexts/ToDosContext";
import { Ionicons } from "@expo/vector-icons";
import PointsSlider from "@/components/shared/PointsSlider";
import { IToDo } from "@/hooks/firebase/types/todoData";
import { ImageBackground } from "react-native";
import AddChoreDialog from "@/components/pages/todos/AddChoreDialog";
const ToDoItem = (props: { item: IToDo }) => {
const ToDoItem = (props: { item: IToDo; isSettings?: boolean }) => {
const { updateToDo } = useToDosContext();
const [editing, setEditing] = useState<boolean>(false);
const [points, setPoints] = useState(props.item.points);
const [pointsModalVisible, setPointsModalVisible] = useState<boolean>(false);
const handlePointsChange = (text: string) => {
const numericValue = parseInt(text, 10);
if (!isNaN(numericValue) && numericValue >= 0 && numericValue <= 100) {
setPoints(numericValue);
} else if (text === "") {
setPoints(0);
}
};
return (
<View
centerV
paddingV-10
paddingH-10
paddingH-13
marginV-10
style={{
borderRadius: 22,
borderRadius: 17,
backgroundColor: props.item.done ? "#e0e0e0" : "white",
opacity: props.item.done ? 0.3 : 1,
}}
>
<View paddingB-5 row spread>
<Text
text70R
<AddChoreDialog isVisible={editing} setIsVisible={setEditing} selectedTodo={props.item}/>
<View paddingB-8 row spread>
<Text
text70
style={{
textDecorationLine: props.item.done ? "line-through" : "none",
textDecorationLine: props.item.done ? "line-through" : "none",
fontFamily: "Manrope_500Medium",
fontSize: 15,
}}
>
onPress={() => {
setEditing(true);
}}
>
{props.item.title}
</Text>
</Text>
<Checkbox
value={props.item.done}
containerStyle={{borderWidth: 0.7, borderRadius: 50, borderColor: 'gray', height: 24.64, width: 24.64}}
color="#fd1575"
onValueChange={(value) => {
updateToDo(props.item.id, { done: !props.item.done });
updateToDo({ id: props.item.id, done: !props.item.done });
}}
/>
</View>
<View centerH paddingV-5>
<View centerH paddingV-0>
<View
centerV
height={2}
height={0.7}
width={"100%"}
style={{
backgroundColor: props.item.done ? '#b8b8b8' : "#e7e7e7",
backgroundColor: props.item.done ? "#b8b8b8" : "#e7e7e7",
}}
centerH
/>
</View>
<View centerH row spread>
<View centerV centerH marginT-8 row spread>
{props.item.points && props.item.points > 0 ? (
<View centerV row>
<Ionicons name="gift-outline" size={20} color="#46a80a" />
<Text color="#46a80a">{props.item.points} points</Text>
</View>
<TouchableOpacity
onPress={() => {
if (props.isSettings) {
setPointsModalVisible(true);
}
}}
>
<View centerV row gap-3>
<Ionicons name="gift-outline" size={20} color="#46a80a" />
<Text color="#46a80a" style={{ fontSize: 12, fontFamily: "Manrope_500Medium" }}>
{props.item.points} points
</Text>
</View>
</TouchableOpacity>
) : (
<View />
)}
<View
height={25}
width={25}
backgroundColor="red"
style={{ borderRadius: 50 }}
<ImageBackground
source={require("../../../assets/images/child-picture.png")}
style={{
height: 24.64,
aspectRatio: 1,
borderRadius: 22,
overflow: "hidden",
}}
/>
</View>
<Dialog
visible={pointsModalVisible}
onDismiss={() => setPointsModalVisible(false)}
containerStyle={{
padding: 20,
borderRadius: 15,
backgroundColor: "white",
}}
children={
<View gap-20>
<View row centerV marginL-30>
<Ionicons name="gift-outline" size={22} color="#919191" />
<Text marginH-10 text70>
Reward points
</Text>
</View>
{points && (
<PointsSlider
points={points}
setPoints={setPoints}
handleChange={handlePointsChange}
/>
)}
<View row marginH-30 spread>
<Button
size={ButtonSize.large}
label="Cancel"
backgroundColor="#d9d9d9"
onPress={() => {
setPointsModalVisible(false);
}}
/>
<Button
size={ButtonSize.large}
label="Save points"
backgroundColor="#fd1775"
style={{ height: 60, width: 150 }}
onPress={() => {
updateToDo({ id: props.item.id, points: points });
setPointsModalVisible(false);
}}
/>
</View>
</View>
}
/>
</View>
);
};

View File

@ -1,11 +1,14 @@
import { View, Text } from "react-native-ui-lib";
import React from "react";
import { IToDo, useToDosContext } from "@/contexts/ToDosContext";
import { View, Text, TouchableOpacity, Icon } from "react-native-ui-lib";
import React, { useState } from "react";
import { useToDosContext } from "@/contexts/ToDosContext";
import ToDoItem from "./ToDoItem";
import { format, isToday, isTomorrow } from "date-fns";
import { format, isToday, isTomorrow } from "date-fns";
import { AntDesign } from "@expo/vector-icons";
import { IToDo } from "@/hooks/firebase/types/todoData";
const groupToDosByDate = (toDos: IToDo[]) => {
return toDos.reduce((groups, toDo) => {
let sortedTodos = toDos.sort((a, b) => a.date - b.date);
return sortedTodos.reduce((groups, toDo) => {
let dateKey;
if (toDo.date === null) {
@ -27,27 +30,73 @@ const groupToDosByDate = (toDos: IToDo[]) => {
}, {} as { [key: string]: IToDo[] });
};
const ToDosList = () => {
const ToDosList = ({ isSettings }: { isSettings?: boolean }) => {
const { toDos } = useToDosContext();
const groupedToDos = groupToDosByDate(toDos);
const [expandedGroups, setExpandedGroups] = useState<{
[key: string]: boolean;
}>({});
const [expandNoDate, setExpandNoDate] = useState<boolean>(true);
const toggleExpand = (dateKey: string) => {
setExpandedGroups((prev) => ({
...prev,
[dateKey]: !prev[dateKey],
}));
};
const noDateToDos = groupedToDos["No Date"] || [];
const datedToDos = Object.keys(groupedToDos).filter((key) => key !== "No Date");
const datedToDos = Object.keys(groupedToDos).filter(
(key) => key !== "No Date"
);
return (
<View marginB-140>
<View marginB-402>
{noDateToDos.length > 0 && (
<View key="No Date">
{noDateToDos
.sort((a, b) => Number(a.done) - Number(b.done))
.map((item) => (
<ToDoItem key={item.id} item={item} />
))}
<View row spread paddingH-19 marginB-12>
<Text
text70
style={{
fontWeight: "bold",
}}
>
Unscheduled
</Text>
{!expandNoDate && (
<AntDesign
name="caretright"
size={24}
color="#fd1575"
onPress={() => {
setExpandNoDate(!expandNoDate);
}}
/>
)}
{expandNoDate && (
<AntDesign
name="caretdown"
size={24}
color="#fd1575"
onPress={() => {
setExpandNoDate(!expandNoDate);
}}
/>
)}
</View>
{expandNoDate &&
noDateToDos
.sort((a, b) => Number(a.done) - Number(b.done))
.map((item) => (
<ToDoItem isSettings={isSettings} key={item.id} item={item} />
))}
</View>
)}
{datedToDos.map((dateKey) => {
const isExpanded = expandedGroups[dateKey] || false;
const sortedToDos = [
...groupedToDos[dateKey].filter((toDo) => !toDo.done),
...groupedToDos[dateKey].filter((toDo) => toDo.done),
@ -55,19 +104,38 @@ const ToDosList = () => {
return (
<View key={dateKey}>
<Text
text70
<TouchableOpacity
onPress={() => toggleExpand(dateKey)}
style={{
fontWeight: "bold",
marginVertical: 8,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
marginVertical: 8,
}}
>
{dateKey}
</Text>
{sortedToDos.map((item) => (
<ToDoItem key={item.id} item={item} />
))}
<Text
text70
style={{
fontFamily: "Manrope_700Bold",
fontSize: 15,
color: "#484848",
}}
>
{dateKey}
</Text>
{!isExpanded && (
<AntDesign name="caretright" size={24} color="#fd1575" />
)}
{isExpanded && (
<AntDesign name="caretdown" size={24} color="#fd1575" />
)}
</TouchableOpacity>
{isExpanded &&
sortedToDos.map((item) => (
<ToDoItem isSettings={isSettings} key={item.id} item={item} />
))}
</View>
);
})}

View File

@ -4,7 +4,7 @@ import HeaderTemplate from "@/components/shared/HeaderTemplate";
import AddChore from "./AddChore";
import ProgressCard from "./ProgressCard";
import ToDosList from "./ToDosList";
import { ScrollView } from "react-native";
import { Dimensions, ScrollView } from "react-native";
import { StyleSheet } from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
@ -14,13 +14,16 @@ import UserChoresProgress from "./user-chores/UserChoresProgress";
const ToDosPage = () => {
const [pageIndex, setPageIndex] = useState<number>(0);
const { profileData } = useAuthContext();
const { width, height } = Dimensions.get("screen");
const pageLink = (
<TouchableOpacity onPress={() => setPageIndex(1)}>
<Text color="#ea156d">View family progress</Text>
<Text color="#ea156d" style={{ fontSize: 14 }}>
View family progress
</Text>
</TouchableOpacity>
);
return (
<View paddingH-25 backgroundColor="#f9f8f7" height={"100%"}>
<View paddingH-25 backgroundColor="#f9f8f7" height={"100%"} width={width}>
{pageIndex == 0 && (
<View>
<ScrollView

View File

@ -1,6 +1,6 @@
import { View, Text } from "react-native-ui-lib";
import React from "react";
import { StyleSheet } from "react-native";
import { ImageBackground, StyleSheet } from "react-native";
import FamilyChart from "./FamilyChart";
import { TouchableOpacity } from "react-native-ui-lib/src/incubator";
@ -12,34 +12,116 @@ const FamilyChoresProgress = ({
return (
<View marginT-20 marginH-5>
<TouchableOpacity onPress={() => setPageIndex(0)}>
<Text>Back to ToDos</Text>
<Text style={{ fontFamily: "Manrope_200", fontSize: 12 }}>Back to ToDos</Text>
</TouchableOpacity>
<View centerH>
<Text text50R>Family Chores Progress Report</Text>
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 19 }}>
Family Chores Progress Report
</Text>
</View>
<View row spread marginT-25 marginB-20>
<Text text70>Points earned this week</Text>
<View row spread marginT-35 marginB-20>
<Text text70 style={{ fontFamily: "Manrope_600SemiBold" }}>
Points earned this week
</Text>
<View row>
<View style={styles.pfpSmall} backgroundColor="#05a8b6" />
<View style={styles.pfpSmall} backgroundColor="#ebd825" />
<View
style={styles.pfpSmall}
backgroundColor="#05a8b6"
center
children={
<ImageBackground
source={require("../../../../assets/images/child1-picture.png")}
style={{
height: 25,
aspectRatio: 1,
borderRadius: 22,
overflow: "hidden",
}}
/>
}
/>
<View
style={styles.pfpSmall}
backgroundColor="#ebd825"
center
children={
<ImageBackground
source={require("../../../../assets/images/child-picture.png")}
style={{
height: 25,
aspectRatio: 1,
borderRadius: 22,
overflow: "hidden",
}}
/>
}
/>
</View>
</View>
<View style={styles.card} paddingL-10>
<FamilyChart />
</View>
<Text text70 marginV-20>
<Text text70 style={{ fontFamily: "Manrope_600SemiBold" }} marginV-20>
Chore Tracker
</Text>
<View style={styles.card} marginB-20 row spread>
<View style={styles.pfpBig} backgroundColor="#05a8b6" />
<View width={"100%"} centerV centerH>
<Text> x/y chores completed</Text>
<View style={styles.card} marginB-20 row spread centerV>
<View
style={styles.pfpBig}
backgroundColor="#05a8b6"
center
children={
<ImageBackground
source={require("../../../../assets/images/child1-picture.png")}
style={{
height: 45,
aspectRatio: 1,
borderRadius: 22,
overflow: "hidden",
}}
/>
}
/>
<Text
color="#858585"
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 17 }}
>
Emily
</Text>
<View centerV>
<Text style={{ fontSize: 16, fontFamily: "Manrope_700Bold" }}>
{" "}
x/y chores completed
</Text>
</View>
</View>
<View style={styles.card} row spread>
<View style={styles.pfpBig} backgroundColor="#ebd825" />
<View width={"100%"} centerV centerH>
<Text> x/y chores completed</Text>
<View style={styles.card} row spread centerV>
<View
style={styles.pfpBig}
backgroundColor="#ebd825"
center
children={
<ImageBackground
source={require("../../../../assets/images/child-picture.png")}
style={{
height: 45,
aspectRatio: 1,
borderRadius: 22,
overflow: "hidden",
}}
/>
}
/>
<Text
color="#858585"
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 17 }}
>
Austin
</Text>
<View row centerV centerH>
<Text style={{ fontSize: 16, fontFamily: "Manrope_700Bold" }}>
{" "}
x/y chores completed
</Text>
</View>
</View>
</View>

View File

@ -6,15 +6,16 @@ import {
ButtonSize,
Modal,
Dialog,
TouchableOpacity,
} from "react-native-ui-lib";
import React, { useState } from "react";
import { StyleSheet } from "react-native";
import { TouchableOpacity } from "react-native-ui-lib/src/incubator";
import UserChart from "./UserChart";
import ProgressCard from "../ProgressCard";
import { AntDesign, Feather, Ionicons } from "@expo/vector-icons";
import { ScrollView } from "react-native-gesture-handler";
import { PanViewDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
import FireworksOrangeIcon from "@/assets/svgs/FireworksOrangeIcon";
const UserChoresProgress = ({
setPageIndex,
@ -29,29 +30,41 @@ const UserChoresProgress = ({
showsHorizontalScrollIndicator={false}
>
<TouchableOpacity onPress={() => setPageIndex(0)}>
<Text>Back to ToDos</Text>
<Text style={{ fontSize: 14 }}>Back to ToDos</Text>
</TouchableOpacity>
<View centerH>
<Text text50R>Your To Dos Progress Report</Text>
<View>
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 20 }}>
Your To Do's Progress Report
</Text>
</View>
<View row spread marginT-25 marginB-5>
<Text text70>Daily Goal</Text>
<Text text70 style={{ fontFamily: "Manrope_600SemiBold" }}>
Daily Goal
</Text>
</View>
<ProgressCard />
<View row spread marginT-10 marginB-5>
<Text text70>Points Earned This Week</Text>
<View row spread marginT-15 marginB-8>
<Text text70 style={{ fontFamily: "Manrope_600SemiBold" }}>
Points Earned This Week
</Text>
</View>
<View style={styles.card} paddingL-10>
<UserChart />
</View>
<View row spread marginT-20 marginB-8 centerV>
<Text text70>Total Reward Points</Text>
<Text text70 style={{ fontFamily: "Manrope_600SemiBold" }}>
Total Reward Points
</Text>
<Button
size={ButtonSize.small}
label="Spend my points"
color="#50be0c"
backgroundColor="#ebf2e4"
onPress={() => setModalVisible(true)}
labelStyle={{
fontSize: 13,
fontFamily: "Manrope_400Regular",
}}
iconSource={() => (
<AntDesign
name="gift"
@ -64,8 +77,12 @@ const UserChoresProgress = ({
</View>
<View style={styles.card}>
<View row centerV>
<Ionicons name="flower-outline" size={30} color="#8005eb" />
<Text text70 marginL-5>
<FireworksOrangeIcon color="#8005eb" />
<Text
marginL-8
text70
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
>
You have 1200 points saved!
</Text>
</View>
@ -80,8 +97,8 @@ const UserChoresProgress = ({
}}
/>
<View row spread>
<Text>0</Text>
<Text>5000</Text>
<Text style={{ fontSize: 13, color: "#858585" }}>0</Text>
<Text style={{ fontSize: 13, color: "#858585" }}>5000</Text>
</View>
</View>
</ScrollView>
@ -90,7 +107,7 @@ const UserChoresProgress = ({
onDismiss={() => setModalVisible(false)}
children={
<View style={styles.card} paddingH-35 paddingT-35>
<Text text60 center marginB-35>
<Text text60 center marginB-35 style={{fontFamily: 'Manrope_600SemiBold'}}>
How would you like to spend your points?
</Text>
<Button
@ -99,6 +116,7 @@ const UserChoresProgress = ({
marginB-15
backgroundColor="#05a8b6"
size={ButtonSize.large}
labelStyle={styles.bigButtonText}
/>
<Button
label="Extra Screen Time - 100 pts"
@ -106,6 +124,7 @@ const UserChoresProgress = ({
marginB-15
backgroundColor="#ea156c"
size={ButtonSize.large}
labelStyle={styles.bigButtonText}
/>
<Button
label="Movie Night - 50 pts"
@ -113,6 +132,7 @@ const UserChoresProgress = ({
marginB-15
backgroundColor="#7305d4"
size={ButtonSize.large}
labelStyle={styles.bigButtonText}
/>
<Button
label="Ice Cream Treat - 25 pts"
@ -120,9 +140,12 @@ const UserChoresProgress = ({
marginB-15
backgroundColor="#e28800"
size={ButtonSize.large}
labelStyle={styles.bigButtonText}
/>
<TouchableOpacity onPress={() => setModalVisible(false)}>
<Text text70 center color="#999999">Go back to my to dos</Text>
<Text text70 marginT-20 center color="#999999" style={{fontFamily: 'Manrope_500Medium'}} >
Go back to my to dos
</Text>
</TouchableOpacity>
</View>
}
@ -147,6 +170,9 @@ const styles = StyleSheet.create({
borderRadius: 20,
padding: 20,
},
bigButtonText:{
fontFamily: 'Manrope_400Regular'
}
});
export default UserChoresProgress;

View File

@ -0,0 +1,23 @@
import React from "react";
import { ImageBackground, StyleSheet } from "react-native";
import { View } from "react-native-ui-lib";
import RemoveAssigneeBtn from "./RemoveAssigneeBtn";
const AssigneesDisplay = () => {
return (
<View row marginH-13 marginT-13 gap-20>
<ImageBackground
source={require("../../assets/images/child-picture.png")}
style={{ aspectRatio: 1, width: 58.08, overflow: "hidden" }}
children={<RemoveAssigneeBtn />}
/>
<ImageBackground
source={require("../../assets/images/child1-picture.png")}
style={{ aspectRatio: 1, width: 58.08, overflow: "hidden" }}
children={<RemoveAssigneeBtn />}
/>
</View>
);
};
export default AssigneesDisplay;

View File

@ -15,7 +15,7 @@ const DrawerButton = (props: IDrawerButtonProps) => {
<Button
onPress={props.pressFunc}
label={props.title}
labelStyle={{ fontSize: 14 }}
labelStyle={styles.labelStyle}
color={props.color}
backgroundColor="white"
iconSource={() => (
@ -30,7 +30,7 @@ const DrawerButton = (props: IDrawerButtonProps) => {
)}
style={{
aspectRatio: 1,
borderRadius: 15,
borderRadius: 18.55,
marginBottom: 12,
flexDirection: "column",
justifyContent: "space-between",
@ -43,8 +43,9 @@ export default DrawerButton;
const styles = StyleSheet.create({
iconContainer: {
width: '70%',
width: "70%",
aspectRatio: 1,
borderRadius: 50,
},
labelStyle: { fontSize: 15, fontFamily: "Poppins_400Regular" },
});

View File

@ -1,6 +1,7 @@
import { View, Text } from "react-native-ui-lib";
import React from "react";
import { useAuthContext } from "@/contexts/AuthContext";
import { ImageBackground } from "react-native";
const HeaderTemplate = (props: {
message: string;
@ -9,22 +10,29 @@ const HeaderTemplate = (props: {
link?: React.ReactNode;
}) => {
const { user, profileData } = useAuthContext();
const headerHeight:number = 72;
return (
<View row centerV marginV-15>
<View
backgroundColor="pink"
height={65}
width={65}
style={{ borderRadius: 22 }}
marginR-20
<ImageBackground
source={require("../../assets/images/profile-picture.png")}
style={{
height: headerHeight,
aspectRatio: 1,
borderRadius: 22,
overflow: "hidden",
marginRight: 20,
}}
/>
<View>
<View gap-3>
{props.isWelcome && (
<Text text70L>Welcome, {profileData?.firstName}!</Text>
<Text text70L style={{fontSize: 19, fontFamily: "Manrope_400Regular"}}>Welcome, {profileData?.firstName}!</Text>
)}
<Text text70BL>{props.message}</Text>
<Text text70B style={{ fontSize: 18 , fontFamily: "Manrope_600SemiBold" }}>
{props.message}
</Text>
{props.children && <View>{props.children}</View>}
{props.link && <View>{props.link}</View>}
{props.link && <View marginT-8>{props.link}</View>}
</View>
</View>
);

View File

@ -14,12 +14,15 @@ const PointsSlider = (props: {
onValueChange={(value) => props.setPoints(value)}
minimumValue={0}
step={10}
thumbTintColor="white"
minimumTrackTintColor="#91d5ff"
thumbStyle={{borderWidth: 3, borderColor: '#91d5ff'}}
maximumValue={100}
/>
<View row spread>
<Text>0</Text>
<Text>50</Text>
<Text>100</Text>
<Text style={{fontSize: 13, color: '#858585'}}>0</Text>
<Text style={{fontSize: 13, color: '#858585'}}>50</Text>
<Text style={{fontSize: 13, color: '#858585'}}>100</Text>
</View>
</View>
<View style={{ marginLeft: "auto" }}>

View File

@ -0,0 +1,27 @@
import { Text, StyleSheet } from "react-native";
import React from "react";
import CloseXIcon from "@/assets/svgs/CloseXIcon";
import { View } from "react-native-ui-lib";
const RemoveAssigneeBtn = () => {
return (
<View style={styles.removeBtn} center>
<CloseXIcon />
</View>
);
};
const styles = StyleSheet.create({
removeBtn: {
aspectRatio: 1,
width: 20,
backgroundColor: "#f0efef",
right: 0,
borderRadius: 50,
position: "absolute",
borderWidth: 1,
borderColor: "#7f7f7f",
},
});
export default RemoveAssigneeBtn;

View File

@ -5,10 +5,18 @@ import {useRouter} from "expo-router";
import firestore from "@react-native-firebase/firestore";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
import {useQueryClient} from "react-query";
export enum ProfileType {
"PARENT" = "parent",
"CHILD" = "child",
"CAREGIVER" = "caregiver"
"CAREGIVER" = "caregiver",
FAMILY_DEVICE = "FAMILY_DEVICE"
}
interface IAuthContext {
@ -19,48 +27,120 @@ interface IAuthContext {
refreshProfileData: () => Promise<void>
}
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
Notifications.addNotificationReceivedListener(notification => {
console.log('Notification received:', notification);
});
async function registerForPushNotificationsAsync() {
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
if (Device.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!');
return;
}
const projectId =
Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId;
if (!projectId) {
alert('Project ID not found');
return;
}
try {
const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
console.log('Push Token:', token);
return token;
} catch (error) {
alert(`Error getting push token: ${error}`);
throw error;
}
} else {
alert('Must use a physical device for push notifications');
}
}
const AuthContext = createContext<IAuthContext>(undefined!)
export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) => {
const [user, setUser] = useState<FirebaseAuthTypes.User | null>(null)
const [user, setUser] = useState<FirebaseAuthTypes.User | null>(null);
const [initializing, setInitializing] = useState(true);
const [profileType, setProfileType] = useState<ProfileType | undefined>(undefined);
const [profileData, setProfileData] = useState<UserProfile | undefined>(undefined);
const {replace} = useRouter();
const ready = !initializing;
const {replace} = useRouter()
const ready = !initializing
const queryClient = useQueryClient();
const onAuthStateChanged = async (user: FirebaseAuthTypes.User | null) => {
setUser(user);
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
setUser(authUser);
if (user) {
await refreshProfileData();
if (authUser) {
await refreshProfileData(authUser);
const pushToken = await registerForPushNotificationsAsync();
if (pushToken) {
await savePushTokenToFirestore(authUser.uid, pushToken);
}
}
if (initializing) setInitializing(false);
}
};
const refreshProfileData = async () => {
if (user) {
const refreshProfileData = async (user?: FirebaseAuthTypes.User) => {
const authUser = user ?? auth().currentUser
if (authUser) {
try {
const documentSnapshot = await firestore()
.collection("Profiles")
.doc(user.uid)
.doc(authUser.uid)
.get();
if (documentSnapshot.exists) {
setProfileType(documentSnapshot.data()?.userType);
setProfileData(documentSnapshot.data() as UserProfile);
}
} catch (error) {
console.error("Error fetching user profile data:", error);
setProfileType(undefined);
setProfileData(undefined);
}
}
};
const savePushTokenToFirestore = async (uid: string, token: string) => {
try {
await firestore().collection("Profiles").doc(uid).update({
pushToken: token,
});
} catch (error) {
console.error('Error saving push token to Firestore:', error);
}
};
useEffect(() => {
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
const subscriber = auth().onAuthStateChanged(onAuthStateChangedHandler);
return subscriber;
}, []);
@ -72,12 +152,23 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
useEffect(() => {
if (ready && user) {
replace({pathname: "/(auth)/calendar"})
replace({pathname: "/(auth)/calendar"});
} else if (ready && !user) {
replace({pathname: "/(unauth)"})
replace({pathname: "/(unauth)"});
}
}, [user, ready]);
useEffect(() => {
const sub = Notifications.addNotificationReceivedListener(notification => {
const eventId = notification?.request?.content?.data?.eventId;
if (eventId) {
queryClient.invalidateQueries(['events']);
}
});
return () => sub.remove()
}, []);
if (!ready) {
return null;
}
@ -86,7 +177,8 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
<AuthContext.Provider value={{user, profileType, profileData, setProfileData, refreshProfileData}}>
{children}
</AuthContext.Provider>
)
}
);
};
export const useAuthContext = () => useContext(AuthContext)!;

View File

@ -3,7 +3,7 @@ import React, { createContext, useContext, useState, ReactNode } from "react";
// Define the CalendarEvent interface
export interface CalendarEvent {
id?: number; // Unique identifier for the event
id?: number | string; // Unique identifier for the event
user?: string;
title: string; // Event title or name
description?: string; // Optional description for the event

View File

@ -13,6 +13,7 @@ interface IBrainDumpContext {
isAddingBrainDump: boolean;
setIsAddingBrainDump: (value: boolean) => void;
addBrainDump: (BrainDump: IBrainDump) => void;
deleteBrainDump: (id: number) => void;
}
const BrainDumpContext = createContext<IBrainDumpContext | undefined>(
@ -82,6 +83,12 @@ export const BrainDumpProvider: React.FC<{ children: React.ReactNode }> = ({
);
};
const deleteBrainDump = (id: number) => {
setBrainDumps((prevBrainDumps) =>
prevBrainDumps.filter((BrainDump) => BrainDump.id !== id)
);
};
return (
<BrainDumpContext.Provider
value={{
@ -90,6 +97,7 @@ export const BrainDumpProvider: React.FC<{ children: React.ReactNode }> = ({
isAddingBrainDump,
setIsAddingBrainDump,
addBrainDump,
deleteBrainDump
}}
>
{children}

View File

@ -1,6 +1,9 @@
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { createContext, useContext, useState } from "react";
import {createContext, useContext, useState} from "react";
import fuzzysort from "fuzzysort";
import {useCreateGrocery} from "@/hooks/firebase/useCreateGrocery";
import {IGrocery} from "@/hooks/firebase/types/groceryData";
import {useUpdateGrocery} from "@/hooks/firebase/useUpdateGrocery";
import {useGetGroceries} from "@/hooks/firebase/useGetGroceries";
export enum GroceryFrequency {
Never = "Never",
@ -11,16 +14,6 @@ export enum GroceryFrequency {
Quarterly = "Quarterly",
}
export interface IGrocery {
id: number;
title: string;
category: GroceryCategory;
approved: boolean;
recurring: boolean;
frequency: GroceryFrequency;
bought: boolean;
}
export enum GroceryCategory {
Fruit = "Fruit",
Dairy = "Dairy",
@ -52,10 +45,136 @@ const iconMapping: { [key in GroceryCategory]: MaterialIconNames } = {
[GroceryCategory.Frozen]: "snowflake",
};*/
const initialGroceryList = [
{
id: 0,
title: "Carrots",
category: GroceryCategory.Vegetables,
approved: false,
bought: false,
recurring: false,
frequency: GroceryFrequency.Never,
},
{
id: 1,
title: "Steak",
category: GroceryCategory.Meat,
approved: true,
bought: false,
recurring: false,
frequency: GroceryFrequency.Never,
},
{
id: 2,
title: "Chicken Breast",
category: GroceryCategory.Poultry,
approved: true,
bought: false,
recurring: false,
frequency: GroceryFrequency.Never,
},
{
id: 3,
title: "Greek Yoghurt",
category: GroceryCategory.Dairy,
approved: false,
bought: false,
recurring: false,
frequency: GroceryFrequency.Never,
},
];
const groceryExamples = {
[GroceryCategory.Fruit]: [
'apple', 'apples', 'banana', 'bananas', 'orange', 'oranges', 'grape', 'grapes',
'pear', 'pears', 'pineapple', 'pineapples', 'kiwi', 'kiwis', 'strawberry',
'strawberries', 'blueberry', 'blueberries', 'mango', 'mangoes', 'watermelon',
'watermelons', 'peach', 'peaches', 'plum', 'plums', 'cherry', 'cherries',
'raspberry', 'raspberries', 'blackberry', 'blackberries', 'pomegranate',
'pomegranates', 'lemon', 'lemons', 'lime', 'limes', 'coconut', 'coconuts'
],
[GroceryCategory.Dairy]: [
'milk', 'whole milk', 'skim milk', 'almond milk', 'soy milk', 'cheese',
'cheeses', 'yoghurt', 'yogurt', 'greek yoghurt', 'greek yogurt', 'butter',
'margarine', 'cream', 'whipping cream', 'heavy cream', 'ice cream', 'ice creams',
'sour cream', 'whipped cream', 'cream cheese', 'cream cheeses', 'buttermilk',
'cottage cheese', 'ghee', 'kefir'
],
[GroceryCategory.Vegetables]: [
'carrot', 'carrots', 'broccoli', 'lettuce', 'lettuces', 'spinach', 'kale',
'cabbage', 'cabbages', 'cauliflower', 'zucchini', 'zucchinis', 'onion', 'onions',
'garlic', 'pepper', 'peppers', 'bell pepper', 'bell peppers', 'tomato', 'tomatoes',
'cucumber', 'cucumbers', 'potato', 'potatoes', 'sweet potato', 'sweet potatoes',
'beet', 'beets', 'eggplant', 'eggplants', 'celery', 'radish', 'radishes',
'asparagus', 'mushroom', 'mushrooms'
],
[GroceryCategory.Meat]: [
'steak', 'steaks', 'beef', 'pork', 'lamb', 'veal', 'ribeye', 'tenderloin',
'filet mignon', 'bacon', 'ham', 'sausage', 'sausages', 'salami', 'ground beef',
'beef mince', 'hamburger meat', 'prosciutto', 'brisket', 'pork chop', 'pork chops'
],
[GroceryCategory.Poultry]: [
'chicken', 'whole chicken', 'chickens', 'chicken breast', 'chicken breasts',
'breast of chicken', 'chicken thigh', 'chicken thighs', 'chicken leg', 'chicken legs',
'chicken wing', 'chicken wings', 'wing', 'wings', 'drumsticks', 'chicken drumstick',
'turkey', 'whole turkey', 'ground turkey', 'turkey breast', 'turkey breasts',
'turkey leg', 'duck', 'ducks', 'goose', 'quail', 'pheasant'
],
[GroceryCategory.Bakery]: [
'bread', 'breads', 'whole wheat bread', 'sourdough bread', 'bagel', 'bagels',
'croissant', 'croissants', 'muffin', 'muffins', 'buns', 'hamburger bun',
'hamburger buns', 'hotdog bun', 'hotdog buns', 'donut', 'donuts', 'roll', 'rolls',
'dinner roll', 'dinner rolls', 'scone', 'scones', 'toast', 'ciabatta',
'focaccia', 'brioche', 'pita', 'pitas', 'naan', 'baguette', 'baguettes',
'pastry', 'pastries', 'pretzel', 'pretzels'
],
[GroceryCategory.Beverages]: [
'coffee', 'decaf coffee', 'iced coffee', 'cold brew', 'tea', 'iced tea',
'black tea', 'green tea', 'herbal tea', 'juice', 'apple juice', 'orange juice',
'grape juice', 'water', 'sparkling water', 'flavored water', 'soda', 'cola',
'diet soda', 'beer', 'lager', 'ale', 'wine', 'red wine', 'white wine',
'whiskey', 'whisky', 'vodka', 'rum', 'gin', 'smoothie', 'smoothies', 'milkshake',
'milkshakes', 'energy drink', 'energy drinks', 'sports drink', 'sports drinks',
'lemonade', 'sparkling lemonade', 'iced lemonade', 'sparkling water', 'cider',
'hard cider', 'kombucha'
],
[GroceryCategory.Snacks]: [
'chips', 'potato chips', 'tortilla chips', 'corn chips', 'candy', 'candies',
'chocolate', 'chocolates', 'dark chocolate', 'milk chocolate', 'white chocolate',
'cookies', 'popcorn', 'pretzel', 'pretzels', 'nuts', 'mixed nuts', 'almonds',
'cashews', 'trail mix', 'granola bar', 'granola bars', 'protein bar', 'protein bars',
'crackers', 'gummies', 'gummy bears', 'fruit snacks', 'dried fruit',
'peanut butter', 'rice cakes', 'snack cakes'
],
[GroceryCategory.Household]: [
'detergent', 'laundry detergent', 'dish soap', 'dishwasher detergent',
'toilet paper', 'paper towels', 'trash bags', 'bin liners', 'broom', 'mop',
'cleaner', 'surface cleaner', 'multi-purpose cleaner', 'disinfectant', 'bleach',
'fabric softener', 'vacuum bags', 'aluminum foil', 'plastic wrap', 'cling wrap',
'light bulbs', 'batteries', 'laundry soap', 'sponges', 'scrubbers',
'dishwashing liquid'
],
[GroceryCategory.PersonalCare]: [
'shampoo', 'conditioner', 'hair shampoo', 'hair conditioner', 'soap', 'bar soap',
'liquid soap', 'toothpaste', 'toothbrush', 'toothbrushes', 'electric toothbrush',
'deodorant', 'antiperspirant', 'lotion', 'moisturizer', 'razor', 'shaving razor',
'shaving cream', 'body wash', 'face wash', 'sunscreen', 'hair gel', 'hair spray',
'nail polish', 'cotton swabs', 'lip balm', 'chapstick', 'hand cream', 'sanitizer'
],
[GroceryCategory.Frozen]: [
'ice cream', 'ice creams', 'frozen pizza', 'frozen pizzas', 'frozen vegetables',
'frozen veg', 'frozen peas', 'frozen fruit', 'frozen fish', 'frozen chicken',
'frozen wings', 'frozen fries', 'frozen french fries', 'frozen shrimp',
'frozen prawns', 'frozen dumplings', 'frozen waffles', 'frozen pancakes',
'frozen pie', 'frozen pies', 'frozen lasagna', 'frozen burrito', 'frozen burritos',
'frozen nuggets', 'frozen pastry', 'frozen pastries', 'frozen meals'
],
};
interface IGroceryContext {
groceries: IGrocery[];
//iconMapping: { [key in GroceryCategory]: MaterialIconNames };
updateGroceryItem: (id: number, changes: Partial<IGrocery>) => void;
updateGroceryItem: (changes: Partial<IGrocery>) => void;
isAddingGrocery: boolean;
setIsAddingGrocery: (value: boolean) => void;
addGrocery: (grocery: IGrocery) => void;
@ -68,142 +187,14 @@ export const GroceryProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [isAddingGrocery, setIsAddingGrocery] = useState<boolean>(false);
const [groceries, setGroceries] = useState<IGrocery[]>([
{
id: 0,
title: "Carrots",
category: GroceryCategory.Vegetables,
approved: false,
bought: false,
recurring: false,
frequency: GroceryFrequency.Never,
},
{
id: 1,
title: "Steak",
category: GroceryCategory.Meat,
approved: true,
bought: false,
recurring: false,
frequency: GroceryFrequency.Never,
},
{
id: 2,
title: "Chicken Breast",
category: GroceryCategory.Poultry,
approved: true,
bought: false,
recurring: false,
frequency: GroceryFrequency.Never,
},
{
id: 3,
title: "Greek Yoghurt",
category: GroceryCategory.Dairy,
approved: false,
bought: false,
recurring: false,
frequency: GroceryFrequency.Never,
},
]);
const { mutateAsync: createGrocery } = useCreateGrocery();
const { mutateAsync: updateGrocery } = useUpdateGrocery();
const { data: groceries } = useGetGroceries();
const addGrocery = (grocery: IGrocery) => {
setGroceries((prevGroceries) => [
...prevGroceries,
{
...grocery,
id: prevGroceries.length
? prevGroceries[prevGroceries.length - 1].id + 1
: 0,
},
]);
};
const groceryExamples = {
[GroceryCategory.Fruit]: [
'apple', 'apples', 'banana', 'bananas', 'orange', 'oranges', 'grape', 'grapes',
'pear', 'pears', 'pineapple', 'pineapples', 'kiwi', 'kiwis', 'strawberry',
'strawberries', 'blueberry', 'blueberries', 'mango', 'mangoes', 'watermelon',
'watermelons', 'peach', 'peaches', 'plum', 'plums', 'cherry', 'cherries',
'raspberry', 'raspberries', 'blackberry', 'blackberries', 'pomegranate',
'pomegranates', 'lemon', 'lemons', 'lime', 'limes', 'coconut', 'coconuts'
],
[GroceryCategory.Dairy]: [
'milk', 'whole milk', 'skim milk', 'almond milk', 'soy milk', 'cheese',
'cheeses', 'yoghurt', 'yogurt', 'greek yoghurt', 'greek yogurt', 'butter',
'margarine', 'cream', 'whipping cream', 'heavy cream', 'ice cream', 'ice creams',
'sour cream', 'whipped cream', 'cream cheese', 'cream cheeses', 'buttermilk',
'cottage cheese', 'ghee', 'kefir'
],
[GroceryCategory.Vegetables]: [
'carrot', 'carrots', 'broccoli', 'lettuce', 'lettuces', 'spinach', 'kale',
'cabbage', 'cabbages', 'cauliflower', 'zucchini', 'zucchinis', 'onion', 'onions',
'garlic', 'pepper', 'peppers', 'bell pepper', 'bell peppers', 'tomato', 'tomatoes',
'cucumber', 'cucumbers', 'potato', 'potatoes', 'sweet potato', 'sweet potatoes',
'beet', 'beets', 'eggplant', 'eggplants', 'celery', 'radish', 'radishes',
'asparagus', 'mushroom', 'mushrooms'
],
[GroceryCategory.Meat]: [
'steak', 'steaks', 'beef', 'pork', 'lamb', 'veal', 'ribeye', 'tenderloin',
'filet mignon', 'bacon', 'ham', 'sausage', 'sausages', 'salami', 'ground beef',
'beef mince', 'hamburger meat', 'prosciutto', 'brisket', 'pork chop', 'pork chops'
],
[GroceryCategory.Poultry]: [
'chicken', 'whole chicken', 'chickens', 'chicken breast', 'chicken breasts',
'breast of chicken', 'chicken thigh', 'chicken thighs', 'chicken leg', 'chicken legs',
'chicken wing', 'chicken wings', 'wing', 'wings', 'drumsticks', 'chicken drumstick',
'turkey', 'whole turkey', 'ground turkey', 'turkey breast', 'turkey breasts',
'turkey leg', 'duck', 'ducks', 'goose', 'quail', 'pheasant'
],
[GroceryCategory.Bakery]: [
'bread', 'breads', 'whole wheat bread', 'sourdough bread', 'bagel', 'bagels',
'croissant', 'croissants', 'muffin', 'muffins', 'buns', 'hamburger bun',
'hamburger buns', 'hotdog bun', 'hotdog buns', 'donut', 'donuts', 'roll', 'rolls',
'dinner roll', 'dinner rolls', 'scone', 'scones', 'toast', 'ciabatta',
'focaccia', 'brioche', 'pita', 'pitas', 'naan', 'baguette', 'baguettes',
'pastry', 'pastries', 'pretzel', 'pretzels'
],
[GroceryCategory.Beverages]: [
'coffee', 'decaf coffee', 'iced coffee', 'cold brew', 'tea', 'iced tea',
'black tea', 'green tea', 'herbal tea', 'juice', 'apple juice', 'orange juice',
'grape juice', 'water', 'sparkling water', 'flavored water', 'soda', 'cola',
'diet soda', 'beer', 'lager', 'ale', 'wine', 'red wine', 'white wine',
'whiskey', 'whisky', 'vodka', 'rum', 'gin', 'smoothie', 'smoothies', 'milkshake',
'milkshakes', 'energy drink', 'energy drinks', 'sports drink', 'sports drinks',
'lemonade', 'sparkling lemonade', 'iced lemonade', 'sparkling water', 'cider',
'hard cider', 'kombucha'
],
[GroceryCategory.Snacks]: [
'chips', 'potato chips', 'tortilla chips', 'corn chips', 'candy', 'candies',
'chocolate', 'chocolates', 'dark chocolate', 'milk chocolate', 'white chocolate',
'cookies', 'popcorn', 'pretzel', 'pretzels', 'nuts', 'mixed nuts', 'almonds',
'cashews', 'trail mix', 'granola bar', 'granola bars', 'protein bar', 'protein bars',
'crackers', 'gummies', 'gummy bears', 'fruit snacks', 'dried fruit',
'peanut butter', 'rice cakes', 'snack cakes'
],
[GroceryCategory.Household]: [
'detergent', 'laundry detergent', 'dish soap', 'dishwasher detergent',
'toilet paper', 'paper towels', 'trash bags', 'bin liners', 'broom', 'mop',
'cleaner', 'surface cleaner', 'multi-purpose cleaner', 'disinfectant', 'bleach',
'fabric softener', 'vacuum bags', 'aluminum foil', 'plastic wrap', 'cling wrap',
'light bulbs', 'batteries', 'laundry soap', 'sponges', 'scrubbers',
'dishwashing liquid'
],
[GroceryCategory.PersonalCare]: [
'shampoo', 'conditioner', 'hair shampoo', 'hair conditioner', 'soap', 'bar soap',
'liquid soap', 'toothpaste', 'toothbrush', 'toothbrushes', 'electric toothbrush',
'deodorant', 'antiperspirant', 'lotion', 'moisturizer', 'razor', 'shaving razor',
'shaving cream', 'body wash', 'face wash', 'sunscreen', 'hair gel', 'hair spray',
'nail polish', 'cotton swabs', 'lip balm', 'chapstick', 'hand cream', 'sanitizer'
],
[GroceryCategory.Frozen]: [
'ice cream', 'ice creams', 'frozen pizza', 'frozen pizzas', 'frozen vegetables',
'frozen veg', 'frozen peas', 'frozen fruit', 'frozen fish', 'frozen chicken',
'frozen wings', 'frozen fries', 'frozen french fries', 'frozen shrimp',
'frozen prawns', 'frozen dumplings', 'frozen waffles', 'frozen pancakes',
'frozen pie', 'frozen pies', 'frozen lasagna', 'frozen burrito', 'frozen burritos',
'frozen nuggets', 'frozen pastry', 'frozen pastries', 'frozen meals'
],
createGrocery(grocery);
};
const fuzzyMatchGroceryCategory = (groceryTitle: string): GroceryCategory => {
@ -221,12 +212,8 @@ export const GroceryProvider: React.FC<{ children: React.ReactNode }> = ({
return bestMatchCategory;
};
const updateGroceryItem = (id: number, changes: Partial<IGrocery>) => {
setGroceries((prevGroceries) =>
prevGroceries.map((grocery) =>
grocery.id === id ? { ...grocery, ...changes } : grocery
)
);
const updateGroceryItem = (changes: Partial<IGrocery>) => {
updateGrocery(changes);
};
return (

View File

@ -1,14 +1,88 @@
import { createContext, FC, ReactNode, useContext, useState } from "react";
import {IToDo} from "@/hooks/firebase/types/todoData";
import {useGetGroceries} from "@/hooks/firebase/useGetGroceries";
import {useGetTodos} from "@/hooks/firebase/useGetTodos";
import {useCreateGrocery} from "@/hooks/firebase/useCreateGrocery";
import {useCreateTodo} from "@/hooks/firebase/useCreateTodo";
import {useUpdateTodo} from "@/hooks/firebase/useUpdateTodo";
export interface IToDo {
id: number;
title: string;
done: boolean;
date: Date | null;
points?: number;
rotate: boolean;
repeatType: string;
}
const initialTodosList = [
{
id: 0,
title: "Pay: Credit card",
done: false,
date: new Date(),
rotate: true,
repeatType: "Every week",
},
{
id: 1,
title: "Monthly Log story",
done: false,
date: new Date(),
rotate: false,
repeatType: "Every week",
},
{
id: 2,
title: "Write: Arcade Highlights",
done: false,
date: new Date(),
rotate: true,
repeatType: "Every week",
},
{
id: 3,
title: "Dressup: Cat",
done: false,
date: new Date(Date.now() + 86400000),
points: 40,
rotate: false,
repeatType: "Every week",
},
{
id: 4,
title: "Trim: Nails",
done: false,
date: new Date(Date.now() + 86400000),
rotate: false,
repeatType: "Once a Month",
},
{
id: 5,
title: "Monthly Log",
done: false,
date: new Date(Date.now() + 2 * 86400000),
rotate: true,
repeatType: "Once a Month",
},
{
id: 6,
title: "Do it",
done: false,
date: new Date(Date.now() + 3 * 86400000),
rotate: false,
repeatType: "Once a year",
points: 50,
},
{
id: 7,
title: "Buy Nautica Voyage",
done: false,
date: null,
rotate: false,
repeatType: "None",
},
{
id: 8,
title: "Sell Dan's Xbox",
done: false,
date: null,
rotate: false,
repeatType: "None",
points: 10,
},
];
export const repeatOptions = [
{ label: "None", value: "None" },
@ -19,7 +93,7 @@ export const repeatOptions = [
interface IToDosContext {
toDos: IToDo[];
updateToDo: (id: number, changes: Partial<IToDo>) => void;
updateToDo: (changes: Partial<IToDo>) => void;
addToDo: (newToDo: IToDo) => void;
maxPoints: number;
}
@ -29,118 +103,36 @@ const ToDosContext = createContext<IToDosContext>(undefined!);
export const ToDosContextProvider: FC<{ children: ReactNode }> = ({
children,
}) => {
const [toDos, setToDos] = useState<IToDo[]>([
{
id: 0,
title: "Pay: Credit card",
done: false,
date: new Date(),
rotate: true,
repeatType: "Every week",
},
{
id: 1,
title: "Monthly Log story",
done: false,
date: new Date(),
rotate: false,
repeatType: "Every week",
},
{
id: 2,
title: "Write: Arcade Highlights",
done: false,
date: new Date(),
rotate: true,
repeatType: "Every week",
},
{
id: 3,
title: "Dressup: Cat",
done: false,
date: new Date(Date.now() + 86400000),
points: 40,
rotate: false,
repeatType: "Every week",
},
{
id: 4,
title: "Trim: Nails",
done: false,
date: new Date(Date.now() + 86400000),
rotate: false,
repeatType: "Once a Month",
},
{
id: 5,
title: "Monthly Log",
done: false,
date: new Date(Date.now() + 2 * 86400000),
rotate: true,
repeatType: "Once a Month",
},
{
id: 6,
title: "Do it",
done: false,
date: new Date(Date.now() + 3 * 86400000),
rotate: false,
repeatType: "Once a year",
points: 50,
},
{
id: 7,
title: "Buy Nautica Voyage",
done: false,
date: null,
rotate: false,
repeatType: "None",
},
{
id: 8,
title: "Sell Dan's Xbox",
done: false,
date: null,
rotate: false,
repeatType: "None",
points: 10,
},
]);
const { data: toDos } = useGetTodos();
const { mutateAsync: createTodo } = useCreateTodo();
const { mutateAsync: updateTodo } = useUpdateTodo();
const initCalc = (): number => {
return toDos.reduce(
return (toDos ?? []).reduce(
(sum, todo) => sum + (todo.points ? todo.points : 0),
50
);
};
const calculateMaxPoints = () => {
const totalPoints = toDos.reduce(
(sum, todo) => sum + (todo.points ? todo.points : 0),
0
);
let totalPoints = 0;
if (toDos) {
totalPoints = toDos.reduce(
(sum, todo) => sum + (todo.points ? todo.points : 0),
0
);
}
setMaxPoints(totalPoints);
};
const [maxPoints, setMaxPoints] = useState<number>(initCalc);
const updateToDo = (id: number, changes: Partial<IToDo>) => {
setToDos((prevToDos) =>
prevToDos.map((toDo) => (toDo.id === id ? { ...toDo, ...changes } : toDo))
);
calculateMaxPoints();
const updateToDo = (changes: Partial<IToDo>) => {
updateTodo(changes).then(calculateMaxPoints);
};
const addToDo = (newToDo: IToDo) => {
setToDos((prevToDos) => [
...prevToDos,
{
...newToDo,
id: prevToDos.length ? prevToDos[prevToDos.length - 1].id + 1 : 0,
},
]);
calculateMaxPoints();
createTodo(newToDo).then(calculateMaxPoints);
};
return (

View File

@ -5,12 +5,17 @@
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
"distribution": "internal",
"channel": "development"
},
"preview": {
"distribution": "internal"
"distribution": "internal",
"channel": "preview"
},
"production": {}
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {

View File

@ -1,14 +1,109 @@
const {onRequest} = require("firebase-functions/v2/https");
const {getAuth} = require("firebase-admin/auth");
const {getFirestore} = require("firebase-admin/firestore");
const admin = require("firebase-admin");
const logger = require("firebase-functions/logger");
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {Expo} = require('expo-server-sdk');
try {
admin.initializeApp();
} catch (error) {
console.error(error)
}
admin.initializeApp();
const db = admin.firestore();
let expo = new Expo({accessToken: process.env.EXPO_ACCESS_TOKEN});
// Firestore trigger that listens for new events in the 'Events' collection
exports.sendNotificationOnEventCreation = functions.firestore
.document('Events/{eventId}')
.onCreate(async (snapshot, context) => {
const eventData = snapshot.data();
const { familyId, creatorId } = eventData;
if (!familyId || !creatorId) {
console.error('Missing familyId or creatorId in event data');
return;
}
const pushTokens = await getPushTokensForFamilyExcludingCreator(familyId, creatorId);
if (!pushTokens.length) {
console.log('No push tokens available for the event.');
return;
}
let messages = [];
for (let pushToken of pushTokens) {
if (!Expo.isExpoPushToken(pushToken)) {
console.error(`Push token ${pushToken} is not a valid Expo push token`);
continue;
}
messages.push({
to: pushToken,
sound: 'default',
title: 'New Event Added!',
body: `An event "${eventData.title}" has been added. Check it out!`,
data: { eventId: context.params.eventId },
});
}
let chunks = expo.chunkPushNotifications(messages);
let tickets = [];
for (let chunk of chunks) {
try {
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
tickets.push(...ticketChunk);
for (let ticket of ticketChunk) {
if (ticket.status === 'ok') {
console.log('Notification successfully sent:', ticket.id);
} else if (ticket.status === 'error') {
console.error(`Notification error: ${ticket.message}`);
if (ticket.details && ticket.details.error) {
console.error('Error details:', ticket.details.error);
if (ticket.details.error === 'DeviceNotRegistered') {
console.log(`Removing invalid push token: ${ticket.to}`);
await removeInvalidPushToken(ticket.to);
}
}
}
}
} catch (error) {
console.error('Error sending notification:', error);
}
}
// Retrieve and handle notification receipts
let receiptIds = [];
for (let ticket of tickets) {
if (ticket.id) {
receiptIds.push(ticket.id);
}
}
let receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds);
for (let chunk of receiptIdChunks) {
try {
let receipts = await expo.getPushNotificationReceiptsAsync(chunk);
console.log('Receipts:', receipts);
for (let receiptId in receipts) {
let { status, message, details } = receipts[receiptId];
if (status === 'ok') {
console.log(`Notification with receipt ID ${receiptId} was delivered successfully`);
} else if (status === 'error') {
console.error(`Notification error: ${message}`);
if (details && details.error) {
console.error(`Error details: ${details.error}`);
}
}
}
} catch (error) {
console.error('Error retrieving receipts:', error);
}
}
return null;
});
exports.createSubUser = onRequest(async (request, response) => {
const authHeader = request.get('Authorization');
@ -37,7 +132,7 @@ exports.createSubUser = onRequest(async (request, response) => {
const {userType, firstName, lastName, email, password, familyId} = request.body.data;
if (!email || !password || !firstName || !lastName || !userType || !familyId) {
if (!email || !password || !firstName || !userType || !familyId) {
logger.warn("Missing required fields in request body", {requestBody: request.body.data});
response.status(400).json({error: "Missing required fields"});
return;
@ -97,3 +192,35 @@ exports.generateCustomToken = onRequest(async (request, response) => {
response.status(500).json({error: "Failed to generate custom token"});
}
});
async function getPushTokensForEvent() {
const usersRef = db.collection('Profiles');
const snapshot = await usersRef.get();
let pushTokens = [];
snapshot.forEach(doc => {
const data = doc.data();
if (data.pushToken) {
pushTokens.push(data.pushToken);
}
});
console.log('Push Tokens:', pushTokens);
return pushTokens;
}
async function getPushTokensForFamilyExcludingCreator(familyId, creatorId) {
const usersRef = db.collection('Profiles');
const snapshot = await usersRef.where('familyId', '==', familyId).get();
let pushTokens = [];
snapshot.forEach(doc => {
const data = doc.data();
// Exclude the creator
if (data.uid !== creatorId && data.pushToken) {
pushTokens.push(data.pushToken);
}
});
return pushTokens;
}

View File

@ -6,6 +6,7 @@
"": {
"name": "functions",
"dependencies": {
"expo-server-sdk": "^3.11.0",
"firebase-admin": "^12.1.0",
"firebase-functions": "^5.0.0"
},
@ -3115,6 +3116,12 @@
"once": "^1.4.0"
}
},
"node_modules/err-code": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"license": "MIT"
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -3425,6 +3432,16 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/expo-server-sdk": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/expo-server-sdk/-/expo-server-sdk-3.11.0.tgz",
"integrity": "sha512-EGH82ZcdAFjKq+6daDE8Xj7BjaSeP1VDvZ3Hmtn/KzEQ3ffqHkauMsgXL2wLEPlvatLq3EsYNcejXRBV54WnFQ==",
"dependencies": {
"node-fetch": "^2.6.0",
"promise-limit": "^2.7.0",
"promise-retry": "^2.0.1"
}
},
"node_modules/express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
@ -5754,7 +5771,6 @@
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"optional": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
@ -6183,6 +6199,34 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/promise-limit": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
"integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==",
"license": "ISC"
},
"node_modules/promise-retry": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
"integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
"license": "MIT",
"dependencies": {
"err-code": "^2.0.2",
"retry": "^0.12.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/promise-retry/node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@ -7048,8 +7092,7 @@
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/ts-api-utils": {
"version": "1.3.0",
@ -7268,8 +7311,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause",
"optional": true
"license": "BSD-2-Clause"
},
"node_modules/websocket-driver": {
"version": "0.7.4",
@ -7299,7 +7341,6 @@
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"optional": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"

View File

@ -14,6 +14,7 @@
},
"main": "index.js",
"dependencies": {
"expo-server-sdk": "^3.11.0",
"firebase-admin": "^12.1.0",
"firebase-functions": "^5.0.0"
},

View File

@ -0,0 +1,13 @@
import {GroceryCategory, GroceryFrequency} from "@/contexts/GroceryContext";
export interface IGrocery {
id: string;
title: string;
category: GroceryCategory;
approved: boolean;
recurring: boolean;
frequency: GroceryFrequency;
bought: boolean;
familyId?: string,
creatorId?: string
}

View File

@ -19,6 +19,7 @@ export interface UserProfile {
uid?: string;
googleToken?: string;
microsoftToken?: string;
eventColor?: string
}
export interface ParentProfile extends UserProfile {

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