Merge pull request #1 from urosran/dev

Dev
This commit is contained in:
Milan Paunović
2024-09-21 22:35:17 +03:00
committed by GitHub
99 changed files with 25974 additions and 833 deletions

40
.github/workflows/ci-cd.yml vendored Executable file
View File

@ -0,0 +1,40 @@
name: CI/CD Workflow
env:
EXPO_ASC_API_KEY_PATH: ../AuthKey_F7ZX3C8C69.p8
EXPO_ASC_KEY_ID: F7ZX3C8C69
EXPO_ASC_ISSUER_ID: f7d6175c-75fe-416c-b6d1-0bc9eaf87415
EXPO_APPLE_TEAM_ID: MV9C3PHV87
EXPO_APPLE_TEAM_TYPE: INDIVIDUAL
EXPO_TOKEN: qt2h_4xhuhFB-ArysIkzgpsBtWOrrZ-c_So_S9ch
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
name: Install and build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: npm
- name: Setup Expo and EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: npm ci
- name: Prebuild, Build and Submit
run: npm run prebuild-build-submit-cicd

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ expo-env.d.ts
# @end expo-cli
/ios/GoogleService-Info.plist
/ios/cally.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
expo-env.d.ts

15
.idea/git_toolbox_prj.xml generated Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

View File

@ -1,6 +1,6 @@
{
"expo": {
"name": "cally",
"name": "Cally - Planner",
"slug": "cally",
"version": "1.0.0",
"orientation": "portrait",
@ -15,7 +15,8 @@
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.cally.app",
"googleServicesFile": "./ios/GoogleService-Info.plist"
"googleServicesFile": "./ios/GoogleService-Info.plist",
"buildNumber": "5"
},
"android": {
"adaptiveIcon": {

View File

@ -1,12 +1,202 @@
import React from 'react';
import React from "react";
import { Drawer } from "expo-router/drawer";
import { useSignOut } from "@/hooks/firebase/useSignOut";
import {
DrawerContentScrollView,
DrawerItem,
DrawerItemList,
} from "@react-navigation/drawer";
import { Button, View, Text, ButtonSize } from "react-native-ui-lib";
import { StyleSheet } from "react-native";
import Feather from "@expo/vector-icons/Feather";
import DrawerButton from "@/components/shared/DrawerButton";
import {
AntDesign,
FontAwesome6,
MaterialCommunityIcons,
Octicons,
} from "@expo/vector-icons";
export default function TabLayout() {
const { mutateAsync: signOut } = useSignOut();
return (
<Drawer
initialRouteName={"index"}
screenOptions={{
headerShown: true,
}}/>
drawerStyle: {
width: "90%",
backgroundColor: "#f9f8f7",
height: "100%",
},
}}
drawerContent={(props) => {
return (
<DrawerContentScrollView {...props} style={{ height: "100%" }}>
<View centerH centerV margin-30>
<Text text50>Welcome to Kali</Text>
</View>
<View
style={{
flexDirection: "row",
paddingHorizontal: 30,
}}
>
<View style={{ flex: 1, paddingRight: 5 }}>
<DrawerButton
title={"Calendar"}
color="rgb(7, 184, 199)"
bgColor={"rgb(231, 248, 250)"}
pressFunc={() => props.navigation.navigate("calendar")}
icon={
<Feather
name="calendar"
size={30}
color="rgb(7, 184, 199)"
/>
}
/>
<DrawerButton
color="#50be0c"
title={"Groceries"}
bgColor={"#eef9e7"}
pressFunc={() => props.navigation.navigate("grocery")}
icon={
<MaterialCommunityIcons
name="food-apple-outline"
size={30}
color="#50be0c"
/>
}
/>
</View>
<View style={{ flex: 1 }}>
{/*<DrawerButton
color="#fd1775"
title={"My Reminders"}
bgColor={"#ffe8f2"}
pressFunc={() => props.navigation.navigate("reminders")}
icon={
<FontAwesome6
name="clock-rotate-left"
size={28}
color="#fd1775"
/>
}
/>*/}
<DrawerButton
color="#8005eb"
title={"To Dos"}
bgColor={"#f3e6fd"}
pressFunc={() => props.navigation.navigate("todos")}
icon={
<AntDesign name="checkcircleo" size={30} color="#8005eb" />
}
/>
<DrawerButton
color="#e0ca03"
title={"Brain Dump"}
bgColor={"#fffacb"}
pressFunc={() => props.navigation.navigate("brain_dump")}
icon={<AntDesign name="book" size={30} color="#e0ca03" />}
/>
{/*<DrawerItem label="Logout" onPress={() => signOut()} />*/}
</View>
</View>
<Button
onPress={() => props.navigation.navigate("settings")}
label={"Manage Settings"}
iconSource={() => (
<View
backgroundColor="#ededed"
width={60}
height={60}
style={{ borderRadius: 50 }}
marginR-10
centerV
centerH
>
<Octicons name="gear" size={30} color="#6c645b" />
</View>
)}
backgroundColor="white"
color="#464039"
paddingV-30
marginH-30
marginB-10
borderRadius={15}
style={{ elevation: 1 }}
/>
<Button
size={ButtonSize.large}
marginH-30
paddingV-15
style={{
marginTop: "47%",
backgroundColor: "transparent",
borderWidth: 2,
borderColor: "#fd1775",
}}
label="Sign out of Kali"
color="#fd1775"
onPress={() => signOut()}
/>
</DrawerContentScrollView>
);
}}
>
<Drawer.Screen
name="index"
options={{
drawerLabel: "Calendar",
title: "Calendar",
}}
/>
<Drawer.Screen
name="calendar"
options={{
drawerLabel: "Calendar",
title: "Calendar",
drawerItemStyle: { display: "none" },
}}
/>
<Drawer.Screen
name="brain_dump"
options={{
drawerLabel: "Brain Dump",
title: "Brain Dump",
}}
/>
<Drawer.Screen
name="settings"
options={{
drawerLabel: "Settings",
title: "Settings",
}}
/>
<Drawer.Screen
name="grocery"
options={{
drawerLabel: "Grocery",
title: "Grocery",
}}
/>
<Drawer.Screen
name="reminders"
options={{
drawerLabel: "Reminders",
title: "Reminders",
}}
/>
<Drawer.Screen
name="todos"
options={{
drawerLabel: "To-Do",
title: "To-Do",
}}
/>
</Drawer>
);
}

View File

@ -1,7 +1,14 @@
import BrainDumpPage from "@/components/pages/brain_dump/BrainDumpPage";
import { BrainDumpProvider } from "@/contexts/DumpContext";
import { ScrollView } from "react-native-gesture-handler";
import { View } from "react-native-ui-lib";
export default function Screen() {
return (
<View/>
)
<BrainDumpProvider>
<View>
<BrainDumpPage />
</View>
</BrainDumpProvider>
);
}

View File

@ -1,7 +1,123 @@
import {View} from "react-native-ui-lib";
import React, {useRef, useState} from "react";
import {LayoutChangeEvent} from "react-native";
import {Calendar} from "react-native-big-calendar";
import {Button, Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib";
import {MaterialIcons} from "@expo/vector-icons";
import {AddEventDialog} from "@/components/pages/calendar/AddEventDialog";
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
const modeMap = new Map([
[0, "day"],
[1, "week"],
[2, "month"]
]);
const months = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
export default function Screen() {
return (
<View/>
)
const [calendarHeight, setCalendarHeight] = useState(0);
const [mode, setMode] = useState<"week" | "month" | "day">("week");
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const calendarContainerRef = useRef(null);
const { data: events} = useGetEvents()
const onLayout = (event: LayoutChangeEvent) => {
const {height} = event.nativeEvent.layout;
setCalendarHeight(height);
};
const handleSegmentChange = (index: number) => {
const selectedMode = modeMap.get(index);
if (selectedMode) {
setMode(selectedMode as "week" | "month" | "day");
}
};
const handleMonthChange = (month: string) => {
const currentYear = selectedDate.getFullYear();
const currentDay = selectedDate.getDate();
const newMonthIndex = months.indexOf(month);
// Update the date with the new month while preserving the day and year
const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
setSelectedDate(updatedDate);
};
console.log(events)
return (
<View style={{flex: 1, height: "100%", padding: 10}}>
<View style={{height: 60, justifyContent: "space-evenly", alignItems: "flex-start"}}>
<Text>Welcome Dalia</Text>
<Text>Let's get your week started!</Text>
</View>
<View
style={{flex: 1, backgroundColor: "#fff", borderRadius: 30}}
ref={calendarContainerRef}
onLayout={onLayout}
>
<View style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 20,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
backgroundColor: "#f9f9f9",
marginBottom: 10,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 5,
elevation: 3,
}}>
<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>
<SegmentedControl
segments={[
{label: "D"},
{label: "W"},
{label: "M"}
]}
onChangeIndex={handleSegmentChange}
initialIndex={[...modeMap.entries()].find(([_, value]) => value === mode)?.[0] || 1}
/>
</View>
</View>
{calendarHeight > 0 && (
<Calendar
mode={mode}
events={events ?? []}
height={calendarHeight}
activeDate={selectedDate}
onSwipeEnd={(newDate) => {
console.log(newDate)
setSelectedDate(newDate);
}}
/>
)}
</View>
<AddEventDialog/>
</View>
);
}

View File

@ -1,7 +1,14 @@
import {View} from "react-native-ui-lib";
import { Text, View } from "react-native-ui-lib";
import GroceryList from "@/components/pages/grocery/GroceryList";
import AddGroceryItem from "@/components/pages/grocery/AddGroceryItem";
import { GroceryProvider } from "@/contexts/GroceryContext";
import React from "react";
import GroceryWrapper from "@/components/pages/grocery/GroceryWrapper";
export default function Screen() {
return (
<View/>
)
<GroceryProvider>
<GroceryWrapper />
</GroceryProvider>
);
}

View File

@ -1,70 +1,3 @@
import { Image, StyleSheet, Platform } from 'react-native';
import Screen from "@/app/(auth)/calendar";
import { HelloWave } from '@/components/HelloWave';
import ParallaxScrollView from '@/components/ParallaxScrollView';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
export default function HomeScreen() {
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
headerImage={
<Image
source={require('@/assets/images/partial-react-logo.png')}
style={styles.reactLogo}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Welcome!</ThemedText>
<HelloWave />
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
<ThemedText>
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
Press{' '}
<ThemedText type="defaultSemiBold">
{Platform.select({ ios: 'cmd + d', android: 'cmd + m' })}
</ThemedText>{' '}
to open developer tools.
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
<ThemedText>
Tap the Explore tab to learn more about what's included in this starter app.
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
<ThemedText>
When you're ready, run{' '}
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
</ThemedText>
</ThemedView>
</ParallaxScrollView>
);
}
const styles = StyleSheet.create({
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
stepContainer: {
gap: 8,
marginBottom: 8,
},
reactLogo: {
height: 178,
width: 290,
bottom: 0,
left: 0,
position: 'absolute',
},
});
export default Screen

View File

@ -1,5 +0,0 @@
import {Stack} from "expo-router";
export default function StackLayout () {
return <Stack screenOptions={{headerShown: false}}/>
}

View File

@ -1,7 +0,0 @@
import {View} from "react-native-ui-lib";
export default function Screen() {
return (
<View/>
)
}

View File

@ -1,7 +1,49 @@
import {View} from "react-native-ui-lib";
import { Card, Text, View } from "react-native-ui-lib";
import { StyleSheet } from "react-native";
import RemindersList from "@/components/pages/reminders/remindersList";
import { RemindersProvider } from "@/contexts/RemindersContext";
export default function Screen() {
return (
<View/>
)
<RemindersProvider>
<View height={"100%"} backgroundColor="#fdf8f5">
<View style={styles.banner} height={"27%"} flexS>
<View style={styles.greetingContainer}>
<Text text70L color="white">
Good Morning,
</Text>
<Text color="white" text30BL>
Hello Ryanae!
</Text>
</View>
<View centerV style={styles.footerContainer}>
<Text text30>😇</Text>
</View>
</View>
<RemindersList />
</View>
</RemindersProvider>
);
}
const styles = StyleSheet.create({
banner: {
backgroundColor: "#f68d51",
height: "27%",
},
greetingContainer: {
marginLeft: "8%",
paddingTop: 15,
paddingBottom: 20,
flex: 1,
flexDirection: "column",
justifyContent: "space-between",
},
footerContainer: {
height: 70,
backgroundColor: "#f9b076",
marginTop: "auto",
borderTopLeftRadius: 30,
marginLeft: "8%",
paddingLeft: "5%",
},
});

View File

@ -0,0 +1,5 @@
import { Stack } from "expo-router";
export default function StackLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}

View File

@ -0,0 +1,15 @@
import SettingsPage from "@/components/pages/settings/SettingsPage";
import { AuthContextProvider } from "@/contexts/AuthContext";
import { SettingsContextProvider } from "@/contexts/SettingsContext";
import React from "react";
import { View } from "react-native-ui-lib";
export default function Screen() {
return (
<AuthContextProvider>
<SettingsContextProvider>
<SettingsPage />
</SettingsContextProvider>
</AuthContextProvider>
);
}

View File

@ -1,7 +1,25 @@
import {View} from "react-native-ui-lib";
import AddChore from "@/components/pages/todos/AddChore";
import ProgressCard from "@/components/pages/todos/ProgressCard";
import ToDoItem from "@/components/pages/todos/ToDoItem";
import ToDosList from "@/components/pages/todos/ToDosList";
import HeaderTemplate from "@/components/shared/HeaderTemplate";
import { useAuthContext } from "@/contexts/AuthContext";
import { ToDosContextProvider, useToDosContext } from "@/contexts/ToDosContext";
import { AntDesign } from "@expo/vector-icons";
import { ScrollView } from "react-native-gesture-handler";
import { Button, ButtonSize, View } from "react-native-ui-lib";
export default function Screen() {
return (
<View/>
)
<ToDosContextProvider>
<ScrollView>
<View backgroundColor="#f9f8f7">
<HeaderTemplate message="Here are your To Do's" isWelcome={true} />
<ProgressCard />
<ToDosList />
</View>
</ScrollView>
<AddChore />
</ToDosContextProvider>
);
}

View File

@ -0,0 +1,5 @@
import {Stack} from "expo-router";
export default function Layout() {
return <Stack screenOptions={{title: ""}}/>
}

7
app/(unauth)/index.tsx Normal file
View File

@ -0,0 +1,7 @@
import Entry from "@/components/pages/main/Entry";
export default function Screen() {
return (
<Entry/>
)
}

View File

@ -0,0 +1,5 @@
import Entry from "@/components/pages/main/Entry";
export default function Screen() {
return <Entry />;
}

View File

@ -1,15 +1,16 @@
import { Link, Stack } from 'expo-router';
import {Link, Stack, usePathname} from 'expo-router';
import { StyleSheet } from 'react-native';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
export default function NotFoundScreen() {
const route = usePathname()
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<ThemedView style={styles.container}>
<ThemedText type="title">This screen doesn't exist.</ThemedText>
<ThemedText type="title">This screen doesn't exist. ({route})</ThemedText>
<Link href="/" style={styles.link}>
<ThemedText type="link">Go to home screen!</ThemedText>
</Link>

View File

@ -6,10 +6,25 @@ import { useEffect } from 'react';
import 'react-native-reanimated';
import {useColorScheme} from '@/hooks/useColorScheme';
import {AuthContextProvider} from "@/contexts/AuthContext";
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";
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
const queryClient = new QueryClient()
if (__DEV__) {
functions().useEmulator('localhost', 5001);
firestore().useEmulator("localhost", 5471);
auth().useEmulator("http://localhost:9099");
}
export default function RootLayout() {
const colorScheme = useColorScheme();
const [loaded] = useFonts({
@ -27,6 +42,8 @@ export default function RootLayout() {
}
return (
<QueryClientProvider client={queryClient}>
<AuthContextProvider>
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(auth)" options={{headerShown: false}}/>
@ -34,5 +51,7 @@ export default function RootLayout() {
<Stack.Screen name="+not-found"/>
</Stack>
</ThemeProvider>
</AuthContextProvider>
</QueryClientProvider>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,6 @@
<svg width="22" height="29" viewBox="0 0 22 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.0987 1.15527H3.99274C2.28214 1.15527 0.974037 2.46338 0.974037 4.17397C0.974037 5.88457 2.28214 7.19268 3.99274 7.19268H21.0987V27.3174" stroke="#E0CA03" stroke-width="1.50935" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.0988 27.3181H3.99284C2.28224 27.3181 0.974136 26.01 0.974136 24.2994V4.17468" stroke="#E0CA03" stroke-width="1.50935" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.0952 4.17468H3.98927" stroke="#E0CA03" stroke-width="1.50935" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.00529 7.19421V19.269L9.02399 18.2628L12.0427 19.269V7.19421" stroke="#E0CA03" stroke-width="1.50935" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 900 B

View File

@ -0,0 +1,3 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.82408 9.68081H26.8667M21.3017 15.2478L7.38912 15.2458M12.0266 20.8116L7.38912 20.8109M7.38912 1.33325V4.11577M21.3017 1.33325V4.11577M6.27611 26.3759H22.4147C23.9731 26.3759 24.7523 26.3759 25.3475 26.0726C25.871 25.8059 26.2967 25.3802 26.5635 24.8567C26.8667 24.2615 26.8667 23.4822 26.8667 21.9239V8.5678C26.8667 7.00944 26.8667 6.23026 26.5635 5.63505C26.2967 5.11148 25.871 4.68581 25.3475 4.41905C24.7523 4.11577 23.9731 4.11577 22.4147 4.11577H6.27611C4.71776 4.11577 3.93857 4.11577 3.34336 4.41905C2.81979 4.68581 2.39412 5.11148 2.12736 5.63505C1.82408 6.23026 1.82408 7.00944 1.82408 8.5678V21.9239C1.82408 23.4822 1.82408 24.2615 2.12736 24.8567C2.39412 25.3802 2.81979 25.8059 3.34336 26.0726C3.93857 26.3759 4.71775 26.3759 6.27611 26.3759Z" stroke="#07B8C7" stroke-width="2.12426" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 960 B

View File

@ -0,0 +1,6 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.865936 10.9945C0.865936 6.1843 0.865936 3.77921 2.36027 2.28486C3.85462 0.790527 6.25971 0.790527 11.0699 0.790527H13.6209C18.4311 0.790527 20.8363 0.790527 22.3305 2.28486C23.8249 3.77921 23.8249 6.1843 23.8249 10.9945V16.0965C23.8249 20.9067 23.8249 23.3119 22.3305 24.8061C20.8363 26.3005 18.4311 26.3005 13.6209 26.3005H11.0699C6.25971 26.3005 3.85462 26.3005 2.36027 24.8061C0.865936 23.3119 0.865936 20.9067 0.865936 16.0965V10.9945Z" stroke="#FF9900" stroke-width="1.56"/>
<path d="M7.24954 13.5442H17.4535" stroke="#FF9900" stroke-width="1.56" stroke-linecap="round"/>
<path d="M7.24954 8.44482H17.4535" stroke="#FF9900" stroke-width="1.56" stroke-linecap="round"/>
<path d="M7.24954 18.6456H13.627" stroke="#FF9900" stroke-width="1.56" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 885 B

View File

@ -0,0 +1,6 @@
<svg width="29" height="32" viewBox="0 0 29 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.8005 12.2963C26.9124 10.0271 25.1379 8.15586 22.8178 7.34114C20.2994 6.4576 17.6742 6.95919 15.1816 7.6473C15.1342 6.28557 15.2494 4.9199 15.4428 3.57161C15.5525 2.81074 16.0775 1.23663 14.7752 1.18278C13.6942 1.13793 13.6912 2.58486 13.586 3.32281C13.3825 4.73839 13.2624 6.17294 13.3013 7.60392C12.7024 7.50423 12.1111 7.28131 11.5147 7.15765C10.6252 6.97417 9.71922 6.8595 8.81024 6.85846C7.05862 6.85846 5.32992 7.32265 3.88847 8.33639C1.08929 10.3059 -0.000716866 13.7424 0.0466641 17.0441C0.0984813 20.6192 1.30913 24.0331 3.34596 26.949C4.24496 28.2365 5.2786 29.471 6.55603 30.3999C7.79755 31.3024 9.18423 31.7252 10.6895 31.9406C11.3288 32.0329 11.9795 32.0353 12.6166 31.9311C13.1731 31.8404 13.6941 31.6056 14.246 31.5123C14.6629 31.442 15.2508 31.7701 15.6541 31.8598C16.3417 32.0144 17.0493 32.0463 17.7498 31.9665C20.3132 31.6743 22.344 30.518 24.0308 28.5794C27.7406 24.3159 29.9515 17.7886 27.8005 12.2963ZM26.1102 21.04C25.636 22.7024 24.8856 24.2222 23.9153 25.6482C23.0562 26.9107 22.0605 28.1617 20.784 29.0243C19.6117 29.8156 18.0252 30.2478 16.6116 30.1182C15.8926 30.0529 15.238 29.7074 14.524 29.6395C13.7247 29.5643 12.9858 29.992 12.2064 30.0982C9.14203 30.5151 6.72171 28.4334 5.05987 26.1264C3.02107 23.2952 1.81141 19.9126 1.92656 16.395C2.03229 13.1715 3.569 9.84524 6.92418 8.96263C8.60253 8.5219 10.3701 8.7752 12.023 9.20052C12.7385 9.38351 13.4769 9.68067 14.2194 9.72054C14.7908 9.78634 15.4195 9.5251 15.9595 9.37649C19.1925 8.48697 22.8324 8.27151 25.0836 11.2313C27.1628 13.9642 27.0082 17.8977 26.1102 21.04Z" fill="#50BE0C"/>
<path d="M12.0001 18.2628C11.2227 18.548 10.0241 18.9734 9.56834 19.7167C9.67956 19.5342 9.78972 19.3538 9.56686 19.7187C9.34444 20.0837 9.45417 19.9042 9.56532 19.7217C8.89823 20.8207 9.83759 22.2512 11.1131 22.0687C12.3711 21.8892 12.4907 20.6502 12.4768 19.626C12.4718 19.1992 12.4493 18.7658 12.382 18.3435C12.3391 18.0793 12.2244 18.1801 12.0001 18.2628Z" fill="#50BE0C"/>
<path d="M18.0968 19.2142C17.662 18.91 17.1778 18.6722 16.6922 18.4622C16.5736 18.4108 15.9937 18.0863 15.8601 18.1651C15.878 18.1541 15.8955 18.1436 15.859 18.1656C15.8237 18.1875 15.8406 18.177 15.8586 18.1661C15.7484 18.2364 15.7629 18.7744 15.7544 18.8821C15.7135 19.3912 15.6991 19.9073 15.7454 20.4153C15.8446 21.5328 16.7935 22.4981 17.9733 21.9326C19.1379 21.3747 19.0676 19.8923 18.0968 19.2142Z" fill="#50BE0C"/>
<path d="M12.2234 4.11127C12.3436 0.578091 8.3477 0.0261546 6.92214 0.728675C7.29361 4.80332 10.4025 4.89303 12.2234 4.11127Z" fill="#50BE0C"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,3 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.2919 4.15134L4.29255 4.28209L4.38508 4.1897C9.67686 -1.0946 18.2799 -1.03894 23.6048 4.28609C28.9322 9.61341 28.9856 18.2215 23.6944 23.5127C18.4031 28.8038 9.79511 28.7504 4.46779 23.4231C1.3109 20.2662 0.00733072 15.9596 0.567096 11.8757C0.641751 11.331 1.1438 10.9501 1.68845 11.0247C2.23311 11.0994 2.6141 11.6014 2.53946 12.146C2.06125 15.6348 3.17419 19.3141 5.8755 22.0154C10.4387 26.5787 17.7862 26.6053 22.2867 22.1049C26.7871 17.6045 26.7603 10.257 22.1971 5.6938C17.6362 1.13288 10.2939 1.10385 5.79286 5.59735L5.70012 5.68993L5.83116 5.69059L6.87794 5.69585C7.42768 5.69861 7.87109 6.1465 7.86833 6.69624C7.86556 7.24598 7.41768 7.68939 6.86793 7.68663L3.30416 7.66872C2.75833 7.66597 2.31653 7.22416 2.31378 6.67834L2.29587 3.11455C2.2931 2.56483 2.73652 2.11694 3.28626 2.11416C3.836 2.1114 4.28389 2.55482 4.28665 3.10456L4.2919 4.15134ZM15.0316 13.4196V13.4422L15.0476 13.4582L18.24 16.6506C18.6288 17.0394 18.6288 17.6697 18.24 18.0583L18.2786 18.0969L18.24 18.0583C17.8513 18.447 17.221 18.447 16.8324 18.0583L16.7938 18.0969L16.8324 18.0583L13.0408 14.2668V8.25448C13.0408 7.70474 13.4865 7.25908 14.0362 7.25908C14.5859 7.25908 15.0316 7.70474 15.0316 8.25448V13.4196Z" fill="#FD1775" stroke="white" stroke-width="0.1092"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.0364 29.9455C7.08347 29.9455 0.636375 23.4984 0.636375 15.5455C0.636375 7.59261 7.08347 1.14551 15.0364 1.14551C22.9893 1.14551 29.4364 7.59261 29.4364 15.5455C29.4364 23.4984 22.9893 29.9455 15.0364 29.9455ZM15.0364 28.5055C22.194 28.5055 27.9964 22.7031 27.9964 15.5455C27.9964 8.3879 22.194 2.58551 15.0364 2.58551C7.87876 2.58551 2.07638 8.3879 2.07638 15.5455C2.07638 22.7031 7.87876 28.5055 15.0364 28.5055ZM19.5673 11.4364C19.8484 11.1552 20.3043 11.1552 20.5855 11.4364C20.8667 11.7176 20.8667 12.1734 20.5855 12.4546L13.3855 19.6546C13.1043 19.9358 12.6484 19.9358 12.3673 19.6546L9.48726 16.7746C9.20608 16.4934 9.20608 16.0376 9.48726 15.7564C9.76844 15.4752 10.2243 15.4752 10.5055 15.7564L12.8764 18.1273L19.5673 11.4364Z" fill="#8005EB" stroke="#8005EB" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 905 B

View File

@ -0,0 +1,54 @@
import { ScrollView } from "react-native";
import React, { useState } from "react";
import { View, Text } 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 { TextInput } from "react-native-gesture-handler";
const BrainDumpPage = () => {
const [searchText, setSearchText] = useState<string>("");
return (
<View>
<ScrollView>
<HeaderTemplate message={"Welcome to your notes!"} isWelcome={false} />
<View marginH-25>
<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>
<DumpList searchText={searchText} />
</View>
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
searchField: {
borderWidth: 1,
borderColor: "#9b9b9b",
borderRadius: 18,
height: 48,
paddingLeft: 10,
marginVertical: 20,
},
});
export default BrainDumpPage;

View File

@ -0,0 +1,30 @@
import { View, Text } from "react-native-ui-lib";
import React, { useState } from "react";
import { IBrainDump } from "@/contexts/DumpContext";
import { TouchableOpacity } from "react-native-gesture-handler";
import MoveBrainDump from "./MoveBrainDump";
const BrainDumpItem = (props: { item: IBrainDump }) => {
const [isVisible, setIsVisible] = useState<boolean>(false);
return (
<View>
<TouchableOpacity onPress={() => setIsVisible(true)}>
<View
backgroundColor="white"
marginV-5
padding-15
style={{ borderRadius: 20, elevation: 2 }}
>
<Text text70BL marginB-8>
{props.item.title}
</Text>
<Text text80>{props.item.description}</Text>
</View>
</TouchableOpacity>
<MoveBrainDump item={props.item} isVisible={isVisible} setIsVisible={setIsVisible} />
</View>
);
};
export default BrainDumpItem;

View File

@ -0,0 +1,27 @@
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';
const DumpList = (props: { searchText: string }) => {
const { brainDumps } = useBrainDumpContext();
const filteredBrainDumps = props.searchText.trim() === ""
? brainDumps
: brainDumps.filter((item) =>
item.title.toLowerCase().includes(props.searchText.toLowerCase())
);
return (
<View>
<FlatList
data={filteredBrainDumps}
keyExtractor={(item) => item.title}
renderItem={({ item }) => <BrainDumpItem key={item.title} item={item} />}
/>
</View>
);
};
export default DumpList;

View File

@ -0,0 +1,124 @@
import React, { useState } from "react";
import { Button, Dialog, View, Text, TextField } from "react-native-ui-lib";
import { 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";
const MoveBrainDump = (props: {
item: IBrainDump;
isVisible: boolean;
setIsVisible: (value: boolean) => void;
}) => {
const [description, setDescription] = useState<string>(
props.item.description
);
return (
<Dialog
bottom={true}
height={"90%"}
panDirection={PanningDirectionsEnum.DOWN}
onDismiss={() => props.setIsVisible(false)}
containerStyle={{
borderRadius: 10,
backgroundColor: "white",
width: "100%",
alignSelf: "stretch",
padding: 0,
paddingTop: 3,
margin: 0,
}}
visible={props.isVisible}
>
<View row spread paddingH-10 paddingV-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" />
)}
onPress={() => {
props.setIsVisible(false);
}}
/>
<View row>
<Button
style={styles.topBtn}
iconSource={() => (
<Octicons name="pencil" size={24} color="#919191" />
)}
onPress={() => {}}
/>
<Button
style={styles.topBtn}
iconSource={() => (
<EvilIcons name="trash" size={30} color="#919191" />
)}
onPress={() => {}}
/>
</View>
</View>
<View centerH>
<Text text60R>{props.item.title} </Text>
</View>
<View style={styles.divider} />
<View row marginH-20>
<Entypo
name="text"
size={24}
color="black"
style={{ marginBottom: "auto" }}
/>
<TextField
textAlignVertical="top"
multiline
placeholder="Add description"
numberOfLines={3}
value={description}
onChangeText={(value) => {
setDescription(value);
}}
/>
</View>
<View style={styles.divider} />
</Dialog>
);
};
const styles = StyleSheet.create({
divider: { height: 1, backgroundColor: "#e4e4e4", marginVertical: 15 },
gradient: {
height: "25%",
position: "absolute",
bottom: 0,
width: "100%",
},
buttonContainer: {
position: "absolute",
bottom: 25,
width: "100%",
},
button: {
backgroundColor: "rgb(253, 23, 117)",
paddingVertical: 20,
},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
rotateSwitch: {
marginLeft: 35,
marginBottom: 10,
marginTop: 25,
},
});
export default MoveBrainDump;

View File

@ -0,0 +1,89 @@
import React, {useState} from "react";
import {MaterialIcons} from "@expo/vector-icons";
import {Button, Card, Dialog, PanningProvider, Text, View} from "react-native-ui-lib";
import {TouchableOpacity} from "react-native";
import {ManuallyAddEventModal} from "@/components/pages/calendar/ManuallyAddEventModal";
export const AddEventDialog = () => {
const [show, setShow] = useState(false);
const [showManualInputModal, setShowManualInputModal] = useState(false);
const handleOpenManualInputModal = () => {
setShow(false);
setTimeout(() => {
setShowManualInputModal(true);
}, 500);
};
return (
<>
<Button
style={{
position: "absolute",
bottom: 20,
right: 20,
height: 60,
width: 60,
borderRadius: 30,
backgroundColor: "#fff",
alignItems: 'center',
justifyContent: 'center',
}}
enableShadow
iconSource={() => <MaterialIcons name="add" size={30}/>}
onPress={() => setShow(true)}
/>
<Dialog
visible={show}
onDismiss={() => setShow(false)}
panDirection={PanningProvider.Directions.DOWN}
center
>
<Card style={{padding: 20, justifyContent: 'center', alignItems: "center"}}>
<Text text60>Create a new event</Text>
<View style={{marginTop: 20, alignItems: 'center'}}>
<Button
style={{
marginBottom: 10,
backgroundColor: "#007bff",
}}
onPress={handleOpenManualInputModal}
>
<Text style={{color: "white"}}>Manually Input</Text>
</Button>
<Button
style={{
marginBottom: 10,
backgroundColor: "#007bff",
opacity: 0.5
}}
disabled
>
<Text style={{color: "white"}}>Scan an Image</Text>
</Button>
<Button
style={{
marginBottom: 10,
backgroundColor: "#007bff",
opacity: 0.5
}}
disabled
>
<Text style={{color: "white"}}>Paste in text</Text>
</Button>
</View>
<TouchableOpacity onPress={() => setShow(false)}>
<Text style={{marginTop: 20, color: "#007bff"}}>Go back</Text>
</TouchableOpacity>
</Card>
</Dialog>
<ManuallyAddEventModal show={showManualInputModal} close={() => setShowManualInputModal(false)}/>
</>
)
}

View File

@ -0,0 +1,260 @@
import {
Avatar,
Colors,
DateTimePicker,
LoaderScreen,
Modal,
Picker,
Switch,
Text,
TextField,
View
} from "react-native-ui-lib";
import {ScrollView, TouchableOpacity} from "react-native-gesture-handler";
import {useSafeAreaInsets} from "react-native-safe-area-context";
import {useState} from "react";
import {MaterialIcons} from "@expo/vector-icons";
import {PickerMultiValue} from "react-native-ui-lib/src/components/picker/types";
import {useAuthContext} from "@/contexts/AuthContext";
import {useCreateEvent} from "@/hooks/firebase/useCreateEvent";
import {EventData} from "@/hooks/firebase/types/eventData";
const daysOfWeek = [
{label: "Monday", value: "monday"},
{label: "Tuesday", value: "tuesday"},
{label: "Wednesday", value: "wednesday"},
{label: "Thursday", value: "thursday"},
{label: "Friday", value: "friday"},
{label: "Saturday", value: "saturday"},
{label: "Sunday", value: "sunday"},
];
export const ManuallyAddEventModal = ({show, close}: { show: boolean, close: () => void }) => {
const {user} = useAuthContext()
const insets = useSafeAreaInsets();
const [title, setTitle] = useState<string>("");
const [isAllDay, setIsAllDay] = useState(false);
const [startTime, setStartTime] = useState(new Date());
const [endTime, setEndTime] = useState(new Date())
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date())
const [repeatInterval, setRepeatInterval] = useState<PickerMultiValue>([]);
const {mutateAsync: createEvent, isLoading, isError} = useCreateEvent()
const formatDateTime = (date: Date) => {
return date.toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric'
});
};
const combineDateAndTime = (date: Date, time: Date): Date => {
const combined = new Date(date);
combined.setHours(time.getHours());
combined.setMinutes(time.getMinutes());
combined.setSeconds(0);
combined.setMilliseconds(0);
return combined;
};
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);
} else {
finalStartDate = combineDateAndTime(startDate, startTime);
finalEndDate = combineDateAndTime(endDate, endTime);
}
const eventData: Partial<EventData> = {
title,
startDate: finalStartDate,
endDate: finalEndDate,
repeatDays: repeatInterval.map(x => x.toString()),
allDay: isAllDay
};
await createEvent(eventData)
close();
};
const getRepeatLabel = () => {
const selectedDays = repeatInterval
const allDays = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
const workDays = ["monday", "tuesday", "wednesday", "thursday", "friday"];
const isEveryWorkDay = workDays.every(day => selectedDays.includes(day));
const isEveryDay = allDays.every(day => selectedDays.includes(day));
if (isEveryDay) {
return "Every day";
} else if (isEveryWorkDay && !selectedDays.includes("saturday") && !selectedDays.includes("sunday")) {
return "Every work day";
} else {
return selectedDays
.map(item => daysOfWeek.find(day => day.value === item)?.label)
.join(", ");
}
};
if (isLoading && !isError) {
return (
<Modal
visible={show}
animationType="slide"
onRequestClose={close}
transparent={false}
>
<LoaderScreen message={'Saving event...'} color={Colors.grey40}/>
</Modal>
)
}
return (
<Modal
visible={show}
animationType="slide"
onRequestClose={close}
transparent={false}
>
<View style={{
flex: 1,
backgroundColor: "#fff",
paddingTop: insets.top, // Safe area inset for top
paddingBottom: insets.bottom, // Safe area inset for bottom
paddingLeft: insets.left, // Safe area inset for left
paddingRight: insets.right, // Safe area inset for right
}}>
<View style={{flexDirection: "row", justifyContent: "space-between", padding: 16}}>
<TouchableOpacity onPress={close}>
<Text style={{color: "#007bff"}}>Cancel</Text>
</TouchableOpacity>
<Text style={{fontWeight: "bold", fontSize: 16}}>Add event</Text>
<TouchableOpacity onPress={handleSave}>
<Text style={{color: "#007bff"}}>Save</Text>
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={{paddingHorizontal: 16, paddingTop: 10}}>
<View style={{marginVertical: 10}}>
<TextField
placeholder={'Title'}
floatingPlaceholder
value={title}
onChangeText={setTitle}
showCharCounter
maxLength={200}
fieldStyle={{
borderBottomWidth: 1,
borderBottomColor: 'black',
borderStyle: 'solid',
}}
/>
</View>
<View style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20
}}>
<View style={{flexDirection: "row", alignItems: "center"}}>
<MaterialIcons name="schedule" size={24} color="gray"/>
<Text style={{marginLeft: 10}}>All-day</Text>
</View>
<Switch
value={isAllDay}
onValueChange={(value) => setIsAllDay(value)}
/>
</View>
<View style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20
}}>
<DateTimePicker
mode="date"
dateFormatter={formatDateTime}
value={startDate}
onChange={setStartDate}
/>
{!isAllDay && (
<DateTimePicker
mode="time"
value={startTime}
onChange={setStartTime}
/>
)}
</View>
{!isAllDay && (
<View style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20
}}>
<DateTimePicker
mode="date"
dateFormatter={formatDateTime}
value={endDate}
onChange={setEndDate}
/>
<DateTimePicker
mode="time"
value={endTime}
onChange={setEndTime}
/>
</View>
)}
<View style={{flexDirection: "row", alignItems: "center", marginBottom: 20}}>
<MaterialIcons name="repeat" size={24} color="gray"/>
<Picker
value={repeatInterval}
//@ts-ignore
onChange={(items: PickerMultiValue) => setRepeatInterval(items)}
placeholder="Doest not repeat"
style={{marginLeft: 10, flex: 1}}
mode={Picker.modes.MULTI}
getLabel={getRepeatLabel}
>
{daysOfWeek.map((option) => (
<Picker.Item key={option.value} label={option.label} value={option.value}/>
))}
</Picker>
</View>
<View style={{flexDirection: "row", alignItems: "center", marginBottom: 20}}>
<MaterialIcons name="person-add" size={24} color="gray"/>
<TouchableOpacity style={{marginLeft: 10, flexDirection: "row", alignItems: "center", flex: 1}}>
<Avatar size={40} backgroundColor={Colors.yellow10}/>
<View style={{marginLeft: 10}}>
<Text>Other</Text>
<Text style={{color: "gray"}}>{user?.email}</Text>
</View>
</TouchableOpacity>
</View>
</ScrollView>
</View>
</Modal>
);
};

View File

@ -0,0 +1,92 @@
import { StyleSheet } from "react-native";
import React, { useState } from "react";
import {
Button,
Colors,
Dialog,
Drawer,
Text,
View,
PanningProvider,
} from "react-native-ui-lib";
import { useGroceryContext } from "@/contexts/GroceryContext";
import { FontAwesome6 } from "@expo/vector-icons";
interface AddGroceryItemProps {
visible: boolean;
onClose: () => void;
}
const AddGroceryItem = () => {
const { isAddingGrocery, setIsAddingGrocery } = useGroceryContext();
const [visible, setVisible] = useState<boolean>(false);
const handleShowDialog = () => {
setVisible(true);
};
const handleHideDialog = () => {
setVisible(false);
};
return (
<View
row
spread
paddingH-25
style={{
position: "absolute",
bottom: 20,
width: "100%",
height: 60,
}}
>
<View style={styles.btnContainer} row>
<Button
color="white"
backgroundColor="#fd1775"
label="Add item"
text70L
iconSource={() => <FontAwesome6 name="add" size={18} color="white" />}
style={styles.finishShopBtn}
enableShadow
onPress={() => {setIsAddingGrocery(true)}}
/>
</View>
</View>
);
};
export default AddGroceryItem;
const styles = StyleSheet.create({
container: {
paddingVertical: 10,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
},
inner: {
paddingHorizontal: 20,
display: "flex",
flexDirection: "column",
},
title: {
fontSize: 20,
fontWeight: "400",
textAlign: "center",
},
divider: {
width: "100%",
height: 1,
backgroundColor: "#E0E0E0",
marginVertical: 10,
},
btnContainer: {
width: "100%",
justifyContent: "center",
},
finishShopBtn: {
width: "100%",
},
shoppingBtn: {
flex: 1,
marginHorizontal: 3,
},
});

View File

@ -0,0 +1,35 @@
import React from "react";
import { View, Text, TouchableOpacity } from "react-native-ui-lib";
import { GroceryCategory, useGroceryContext } from "@/contexts/GroceryContext";
import { ScrollView } from "react-native-gesture-handler";
const CategoryDropdown = (props: {
setSelectedCategory: (category: GroceryCategory) => void;
}) => {
const groceryCategories = Object.values(GroceryCategory);
return (
<View height={100}>
<ScrollView>
{groceryCategories.map((category) => (
<TouchableOpacity
onPress={() => {
props.setSelectedCategory(category);
}}
>
<View
key={category}
style={{
padding: 10,
}}
>
<Text>{category}</Text>
</View>
</TouchableOpacity>
))}
</ScrollView>
</View>
);
};
export default CategoryDropdown;

View File

@ -0,0 +1,98 @@
import { StyleSheet } from "react-native";
import React, { useState } from "react";
import {
Dialog,
Text,
View,
PanningProvider,
Switch,
Picker,
PickerValue,
} from "react-native-ui-lib";
import {
GroceryFrequency,
IGrocery,
useGroceryContext,
} from "@/contexts/GroceryContext";
interface EditGroceryFrequencyProps {
visible: boolean;
onClose: () => void;
item: IGrocery;
}
const EditGroceryFrequency = (props: EditGroceryFrequencyProps) => {
const { updateGroceryItem } = useGroceryContext();
const pickerItems = Object.values(GroceryFrequency).map((value) => ({
label: value,
value: value,
}));
return (
<Dialog
visible={props.visible}
onDismiss={props.onClose}
panDirection={PanningProvider.Directions.DOWN}
containerStyle={{ borderRadius: 12, backgroundColor: "white" }}
>
<View style={styles.container}>
<Text style={styles.title}>Edit grocery frequency</Text>
<View style={styles.divider} />
<View style={styles.inner}>
<View row spread>
<Text text70>Recurring</Text>
<Switch
value={props.item.recurring}
onValueChange={(value) =>
updateGroceryItem(props.item.id, { recurring: value })
}
onColor={"lime"}
/>
</View>
<Picker
value={props.item.frequency}
fieldType="form"
useDialog
items={pickerItems}
onChange={(item: PickerValue) => {
const selectedFrequency =
GroceryFrequency[item as keyof typeof GroceryFrequency];
if (selectedFrequency) {
updateGroceryItem(props.item.id, {
frequency: selectedFrequency,
});
} else {
console.error("Invalid frequency selected");
}
}}
/>
</View>
</View>
</Dialog>
);
};
export default EditGroceryFrequency;
const styles = StyleSheet.create({
container: {
paddingVertical: 10,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
},
inner: {
paddingHorizontal: 20,
display: "flex",
flexDirection: "column",
},
title: {
fontSize: 20,
fontWeight: "400",
textAlign: "center",
},
divider: {
width: "100%",
height: 1,
backgroundColor: "#E0E0E0",
marginVertical: 10,
},
});

View File

@ -0,0 +1,28 @@
import { View, Text } from 'react-native'
import React from 'react'
import { TextField } from 'react-native-ui-lib'
import CategoryDropdown from './CategoryDropdown'
import { GroceryCategory } from '@/contexts/GroceryContext'
const EditGroceryItem = (props: {title: string, setTitle: (value: string) => void, setCategory: (category: GroceryCategory) => void}) => {
return (
<View
style={{
backgroundColor: "white",
width: "100%",
borderRadius: 25,
padding: 15,
}}
>
<TextField
placeholder="Grocery"
value={props.title}
onChangeText={(value) => props.setTitle(value)}
maxLength={25}
/>
<CategoryDropdown setSelectedCategory={props.setCategory} />
</View>
)
}
export default EditGroceryItem

View File

@ -0,0 +1,161 @@
import {
View,
Text,
Button,
TouchableOpacity,
Checkbox,
} 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 EditGroceryFrequency from "./EditGroceryFrequency";
import { TextInput } from "react-native";
import EditGroceryItem from "./EditGroceryItem";
const GroceryItem = ({
item,
handleItemApproved,
}: {
item: IGrocery;
handleItemApproved: (id: number, changes: Partial<IGrocery>) => void;
}) => {
const { updateGroceryItem, groceries } = useGroceryContext();
const { profileType } = useAuthContext();
const [openFreqEdit, setOpenFreqEdit] = useState<boolean>(false);
const [isEditingTitle, setIsEditingTitle] = useState<boolean>(false);
const [newTitle, setNewTitle] = useState<string>("");
const handleTitleChange = (newTitle: string) => {
updateGroceryItem(item.id, { title: newTitle });
};
const handleCategoryChange = (newCategory: GroceryCategory) => {
updateGroceryItem(item.id, { category: newCategory });
};
useEffect(() => {
setNewTitle(item.title);
}, []);
return (
<ListItem
style={{ borderRadius: 50, marginVertical: 5, height: 55 }}
backgroundColor="white"
centerV
padding-0
onPress={() => {
setOpenFreqEdit(true);
}}
>
<EditGroceryFrequency
visible={openFreqEdit}
key={item.id}
item={item}
onClose={() => {
setOpenFreqEdit(false);
}}
/>
<ListItem.Part left containerStyle={{ flex: 1, paddingStart: 20 }}>
{/* <View
height={50}
width={50}
style={{ borderRadius: 15 }}
backgroundColor="#e6f1ed"
marginR-10
children={
<MaterialCommunityIcons
name={iconMapping[item.category]}
size={50}
color="orange"
/>
}
/>*/}
{!isEditingTitle ? (
<View>
<TouchableOpacity onPress={() => setIsEditingTitle(true)}>
<Text text70BL>{item.title}</Text>
</TouchableOpacity>
{ /*
<TextInput
value={item.title}
onChangeText={handleTitleChange}
onBlur={() => {
setIsEditingTitle(false);
console.log(groceries);
}}
autoFocus
style={{
fontSize: 16,
padding: 0,
margin: 0,
fontWeight: "bold",
}}
/>
*/}
</View>
) : (
<EditGroceryItem
title={item.title}
setTitle={handleTitleChange}
setCategory={handleCategoryChange}
/>
)}
</ListItem.Part>
<ListItem.Part right containerStyle={{ paddingEnd: 20 }}>
{!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}
color={"#f58749"}
onValueChange={() =>
updateGroceryItem(item.id, { bought: !item.bought })
}
/>
)}
</ListItem.Part>
</ListItem>
);
};
export default GroceryItem;

View File

@ -0,0 +1,198 @@
import { FlatList } from "react-native";
import React, { useEffect, useState } from "react";
import { View, Text, ListItem, Button, TextField } 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 EditGroceryItem from "./EditGroceryItem";
const GroceryList = () => {
const {
groceries,
updateGroceryItem,
isAddingGrocery,
setIsAddingGrocery,
addGrocery,
} = useGroceryContext();
const [approvedGroceries, setapprovedGroceries] = useState<IGrocery[]>(
groceries.filter((item) => item.approved === true)
);
const [pendingGroceries, setPendingGroceries] = useState<IGrocery[]>(
groceries.filter((item) => item.approved !== true)
);
const [category, setCategory] = useState<GroceryCategory>(
GroceryCategory.Bakery
);
const [title, setTitle] = useState<string>("");
// Group approved groceries by category
const approvedGroceriesByCategory = approvedGroceries.reduce(
(groups: any, item: IGrocery) => {
const category = item.category || "Uncategorized";
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(item);
return groups;
},
{}
);
useEffect(() => {
if (title?.length > 2 && title?.length <= 25) {
addGrocery({
id: 0,
title: title,
category: category,
approved: false,
recurring: false,
frequency: GroceryFrequency.Never,
bought: false,
});
setIsAddingGrocery(false);
}
}, [category]);
useEffect(() => {
setapprovedGroceries(groceries.filter((item) => item.approved === true));
setPendingGroceries(groceries.filter((item) => item.approved !== true));
}, [groceries]);
return (
<View marginH-20>
<HeaderTemplate
message={"Welcome to your grocery list"}
isWelcome={false}
>
<View row spread>
<View
backgroundColor="#e2eed8"
padding-8
style={{ borderRadius: 50 }}
>
<Text text70BL color="#46a80a">
{approvedGroceries.length} list{" "}
{approvedGroceries.length === 1 ? (
<Text text70BL color="#46a80a">
item
</Text>
) : (
<Text text70BL color="#46a80a">
items
</Text>
)}
</Text>
</View>
<View
backgroundColor="#faead2"
padding-8
style={{ borderRadius: 50 }}
>
<Text text70BL color="#e28800">
{pendingGroceries.length} pending
</Text>
</View>
<Button
backgroundColor='transparent'
paddingH-10
iconSource={() => (
<MaterialIcons name="person-add-alt" size={24} color="gray" />
)}
/>
</View>
</HeaderTemplate>
{/* Pending Approval Section */}
<View row spread marginT-40 marginB-20 centerV>
<Text text70BL>Pending Approval</Text>
<View
centerV
style={{
aspectRatio: 1,
width: 40,
backgroundColor: "#faead2",
borderRadius: 50,
}}
>
<Text text60L center color="#e28800">
{pendingGroceries.length.toString()}
</Text>
</View>
</View>
{pendingGroceries.length > 0 ? (
<FlatList
data={pendingGroceries}
renderItem={({ item }) => (
<GroceryItem item={item} handleItemApproved={updateGroceryItem} />
)}
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
centerV
style={{
aspectRatio: 1,
width: 40,
backgroundColor: "#e2eed8",
borderRadius: 50,
}}
>
<Text text60L center color="#46a80a">
{approvedGroceries.length.toString()}
</Text>
</View>
</View>
{isAddingGrocery && (
<EditGroceryItem title={title} setTitle={setTitle} setCategory={setCategory} />
)}
{/* 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}
/>
)
)}
</View>
)}
keyExtractor={(category) => category}
/>
) : (
<Text>No approved items.</Text>
)}
</View>
);
};
export default GroceryList;

View File

@ -0,0 +1,22 @@
import { Text, ScrollView } from "react-native";
import { View } from "react-native-ui-lib";
import React from "react";
import AddGroceryItem from "./AddGroceryItem";
import GroceryList from "./GroceryList";
import { useGroceryContext } from "@/contexts/GroceryContext";
const GroceryWrapper = () => {
const { isAddingGrocery } = useGroceryContext();
return (
<View height={"100%"}>
<View height={'90%'}>
<ScrollView>
<GroceryList />
</ScrollView>
</View>
{!isAddingGrocery && <AddGroceryItem />}
</View>
);
};
export default GroceryWrapper;

View File

@ -0,0 +1,60 @@
import { View, Text, Button } from "react-native-ui-lib";
import React, { useEffect, useState } from "react";
import { Octicons } from "@expo/vector-icons";
import { useGroceryContext } from "@/contexts/GroceryContext";
const TopDisplay = () => {
const { groceries, isShopping } = useGroceryContext();
const [approved, setApproved] = useState<number>(0);
const [pending, setPending] = useState<number>(0);
const [notBoughtCound, setNotBoughtCound] = useState<number>(0);
useEffect(() => {
const approvedCount = groceries.filter(
(grocery) => grocery.approved
).length;
const pendingCount = groceries.filter(
(grocery) => !grocery.approved
).length;
const notBoughtCound = groceries.filter(
(grocery) => !grocery.bought
).length;
setApproved(approvedCount);
setPending(pendingCount);
setNotBoughtCound(notBoughtCound);
}, [groceries]);
return (
<View backgroundColor="#e1e1e1" paddingL-20 paddingT-20 paddingB-10>
<Text text50BL marginB-10 color="black">
Welcome to your grocery list!
</Text>
{!isShopping ? (
<View row bottom style={{ justifyContent: "space-between" }}>
<View>
<Text text70BL color="black">
{approved} approved items
</Text>
<Text text70BL color="black">
{pending} pending items
</Text>
</View>
<Button
backgroundColor="#e1e1e1"
right
padding-0
children={<Octicons name="share" size={30} color="black" />}
/>
</View>
) : (
<View>
<Text text70BL color="black">
You have {notBoughtCound} items left in your cart
</Text>
</View>
)}
</View>
);
};
export default TopDisplay;

View File

@ -0,0 +1,29 @@
import { View, Text } from "react-native-ui-lib";
import React, { useState } from "react";
import SignUpPage from "./SignUpPage";
import SignInPage from "./SignInPage";
import { useSignUp } from "@/hooks/firebase/useSignUp";
import { StyleSheet } from "react-native";
const Entry = () => {
const [isRegister, setIsRegister] = useState<boolean>(false);
const { mutateAsync: signUp } = useSignUp();
const setRegister = () => {
setIsRegister(true);
};
const unsetRegister = () => {
setIsRegister(false);
};
return (
<View>
{isRegister ? (
<SignUpPage unsetRegister={unsetRegister} />
) : (
<SignInPage setRegister={setRegister} />
)}
</View>
);
};
export default Entry;

View File

@ -0,0 +1,69 @@
import {Button, ButtonSize, Text, TextField, View} from "react-native-ui-lib";
import React, {useState} from "react";
import {useSignIn} from "@/hooks/firebase/useSignIn";
import {StyleSheet} from "react-native";
const SignInPage = (props: { setRegister: () => any }) => {
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const {mutateAsync: signIn, error, isError} = useSignIn();
const handleSignIn = async () => {
await signIn({email, password});
};
return (
<View padding-10 centerV height={"100%"}>
<TextField
placeholder="Email"
value={email}
onChangeText={setEmail}
style={styles.textfield}
/>
<TextField
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
style={styles.textfield}
/>
<Button
label="Login"
onPress={handleSignIn}
style={{marginBottom: 20}}
backgroundColor="#fd1775"
/>
{isError && <Text center style={{marginBottom: 20}}>{`${error}`}</Text>}
<View row centerH marginB-5 gap-5>
<Text text70>
Don't have an account?
</Text>
<Button
onPress={props.setRegister}
label="Sign Up"
link
size={ButtonSize.xSmall}
padding-0
margin-0
text70
left
color="#fd1775"
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
textfield: {
backgroundColor: "white",
marginVertical: 10,
padding: 30,
height: 45,
borderRadius: 50,
},
});
export default SignInPage;

View File

@ -0,0 +1,145 @@
import React, {useState} from "react";
import {Button, ButtonSize, Text, TextField, View,} from "react-native-ui-lib";
import {useSignUp} from "@/hooks/firebase/useSignUp";
import {ProfileType} from "@/contexts/AuthContext";
import {StyleSheet} from "react-native";
import {AntDesign} from "@expo/vector-icons";
const SignUpPage = (props: { unsetRegister: () => any }) => {
const [email, setEmail] = useState<string>("");
const [firstName, setFirstName] = useState<string>("");
const [lastName, setLastName] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [isParent, setIsParent] = useState<boolean>(true);
const [isChild, setIsChild] = useState<boolean>(false);
const [isCaregiver, setIsCaregiver] = useState<boolean>(false);
const [profileType, setProfileType] = useState<ProfileType>(
ProfileType.PARENT
);
const {mutateAsync: signUp} = useSignUp();
const handleSignUp = async () => {
await signUp({email, password, firstName, lastName});
};
return (
<View padding-10>
<Text text30 center>
Get started with Kali
</Text>
<Text center>Please enter your details.</Text>
<TextField
marginT-60
placeholder="First name"
value={firstName}
onChangeText={setFirstName}
style={styles.textfield}
/>
<TextField
placeholder="Last name"
value={lastName}
onChangeText={setLastName}
style={styles.textfield}
/>
<TextField
placeholder="Email"
value={email}
onChangeText={setEmail}
style={styles.textfield}
/>
<TextField
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
style={styles.textfield}
/>
<Button
label="Register"
onPress={handleSignUp}
style={{marginBottom: 10, backgroundColor: "#fd1775"}}
/>
<Button
label="Sign up with Google"
backgroundColor="white"
color="black"
iconSource={() => (
<AntDesign
name="google"
size={24}
color="black"
style={{marginRight: 15}}
/>
)}
/>
{/*<Text style={{ marginBottom: 10 }}>Choose Profile Type:</Text>
<Checkbox
label="Parent"
value={isParent}
onValueChange={(value) => {
setIsParent(value);
setProfileType(ProfileType.PARENT);
if (value) {
setIsChild(false);
setIsCaregiver(false);
}
}}
style={{ marginBottom: 10 }}
/>
<Checkbox
label="Child"
value={isChild}
onValueChange={(value) => {
setIsChild(value);
setProfileType(ProfileType.CHILD);
if (value) {
setIsParent(false);
setIsCaregiver(false);
}
}}
style={{ marginBottom: 10 }}
/>
<Checkbox
label="Caregiver"
value={isCaregiver}
onValueChange={(value) => {
setIsCaregiver(value);
setProfileType(ProfileType.CAREGIVER);
if (value) {
setIsParent(false);
setIsChild(false);
}
}}
/>*/}
<View row centerH marginT-10 marginB-5 gap-5>
<Text text70 center>
Already have an account?
</Text>
<Button
label="Sign In"
flexS
margin-0
link
color="#fd1775"
size={ButtonSize.small}
text70
onPress={props.unsetRegister}
/>
</View>
</View>
);
};
export default SignUpPage;
const styles = StyleSheet.create({
textfield: {
backgroundColor: "white",
marginVertical: 10,
padding: 30,
height: 45,
borderRadius: 50,
},
});

View File

@ -0,0 +1,22 @@
import { Image } from "react-native";
import React from "react";
import { View, Text, Button } from "react-native-ui-lib";
const WelcomeSplash = () => {
return (
<View>
<Image
source={require("../../../assets/images/splash-clock.png")}
height={10}
width={10}
/>
<Button
label="Continue"
style={{ backgroundColor: "#fd1775" }}
onPress={() => {}}
/>
</View>
);
};
export default WelcomeSplash;

View File

@ -0,0 +1,84 @@
import { Image } from "react-native";
import React, { useRef } from "react";
import { View, Text, Button, TextField } from "react-native-ui-lib";
import Onboarding from "react-native-onboarding-swiper";
import { StyleSheet } from "react-native";
import { useAuthContext } from "@/contexts/AuthContext";
import { useSignUp } from "@/hooks/firebase/useSignUp";
const OnboardingFlow = () => {
const onboardingRef = useRef(null);
const { mutateAsync: signUp } = useSignUp();
return (
<Onboarding
showPagination={false}
ref={onboardingRef}
containerStyles={{ backgroundColor: "#f9f8f7" }}
imageContainerStyles={{
paddingBottom: 0,
paddingTop: 0,
}}
pages={[
{
backgroundColor: "#f9f8f7",
image: (
<Image
source={require("../../../assets/images/splash-clock.png")}
height={10}
width={10}
/>
),
title: <Text text30>Welcome to Kali</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)}
/>
</View>
),
},
{
backgroundColor: "#f9f8f7",
title: <Text>Get started with Kali</Text>,
subtitle: (
<View
style={{
marginBottom: "auto",
width: "100%",
}}
>
<View marginH-30>
{/*<TextField style={styles.textfield} placeholder="First name" />*/}
{/*<TextField style={styles.textfield} placeholder="Last name" />*/}
<TextField style={styles.textfield} placeholder="Email" />
<TextField style={styles.textfield} placeholder="Password" />
<Button
label="Login"
backgroundColor="#ea156c"
onPress={() => {
console.log("Onboarding Done");
}}
/>
</View>
</View>
),
},
]}
/>
);
};
export default OnboardingFlow;
const styles = StyleSheet.create({
textfield: {
backgroundColor: "white",
marginVertical: 10,
padding: 30,
height: 45,
borderRadius: 50,
},
});

View File

@ -0,0 +1,306 @@
import { StyleSheet } from "react-native";
import React, { useState } from "react";
import {
Dialog,
Text,
PanningProvider,
TextField,
Button,
View,
Switch,
Picker,
} from "react-native-ui-lib";
import ButtonOutlined from "@/components/ui/ButtonOutlined";
import AntDesign from "@expo/vector-icons/AntDesign";
import Feather from "@expo/vector-icons/Feather";
import Ionicons from "@expo/vector-icons/Ionicons";
import { DateTimePicker } from "react-native-ui-lib";
import { TouchableOpacity } from "react-native-gesture-handler";
interface ReminderModalProps {
visible: boolean;
onClose: () => void;
onSave: (newReminder: Reminder) => void;
}
interface Reminder {
title: string;
date: Date;
done: boolean;
isAutoRepeat: boolean;
remindIn: string;
}
interface reminderOptions {
label: string;
value: string;
}
const AddReminderModal = (props: ReminderModalProps) => {
const { visible, onClose , onSave } = props;
const [reminderTitle, setReminderTitle] = useState<string>("");
const [time, setTime] = useState(new Date());
const [autoRepeat, setAutoRepeat] = useState<boolean>(false);
const [reminder, setReminder] = useState<string>("just-in-time");
const reminderOptions = [
{ label: "Remind me just in time", value: "just-in-time" },
{ label: "Remind me 5 minutes before", value: "5-min-before" },
{ label: "Remind me 10 minutes before", value: "10-min-before" },
{ label: "Remind me 15 minutes before", value: "15-min-before" },
{ label: "Remind me 30 minutes before", value: "30-min-before" },
{ label: "Remind me 1 hour before", value: "1-hr-before" },
{ label: "Remind me 1 day before", value: "1-day-before" },
];
const handleSave = () => {
const newReminder: Reminder = {
title: reminderTitle,
date: time,
done: false,
isAutoRepeat: autoRepeat,
remindIn: reminder
};
onSave(newReminder);
onClose();
};
const handleDateTimeChange = (selectedTime: Date) => {
setTime(selectedTime);
};
return (
<Dialog
visible={visible}
onDismiss={onClose}
panDirection={PanningProvider.Directions.RIGHT}
containerStyle={{ borderRadius: 12, backgroundColor: "white" }}
>
<View style={styles.container}>
<Text style={styles.title}>New Reminder</Text>
<View style={styles.divider} />
<View style={styles.inner}>
<View
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 10,
}}
>
<Text text70>Profiles</Text>
<ButtonOutlined
title={"+ Tag"}
size={Button.sizes.xSmall}
onPress={() => {}}
/>
</View>
<View style={styles.iconTitle}>
<View
style={{
height: 27,
width: 27,
marginRight: 10,
borderRadius: 50,
backgroundColor: "#fcf4eb",
alignItems: "center",
justifyContent: "center",
}}
>
<Feather name="scissors" size={14} color="black" />
</View>
<TextField
value={reminderTitle}
onChangeText={(text) => setReminderTitle(text)}
placeholder="Reminder Title"
text70L
/>
</View>
</View>
<View style={styles.divider} />
<View style={styles.inner}>
<Text text70>Reminder Time</Text>
<View style={styles.inputsBetween}>
<DateTimePicker
migrateDialog
value={time}
mode="date"
leadingAccessory={
<View>
<Feather name="calendar" size={20} color="black" />
<View style={{ marginRight: "75%" }} />
</View>
}
onChange={handleDateTimeChange}
text70
/>
</View>
<TouchableOpacity>
<View style={styles.inputsBetween}>
<DateTimePicker
migrateDialog
value={time}
mode="time"
is24Hour={false}
onChange={handleDateTimeChange}
leadingAccessory={
<View>
<AntDesign name="clockcircleo" size={20} color="black" />
<View style={{ marginRight: "70%" }} />
</View>
}
text70
/>
</View>
</TouchableOpacity>
<View style={styles.inputsStart}>
<Picker
text70
value={reminder}
onChange={(item) => setReminder(item)}
useDialog
fieldType="form"
placeholder="Select a reminder"
leadingAccessory={
<Feather
name="bell"
size={19}
color="black"
style={{ marginRight: 7 }}
/>
}
trailingAccessory={
<Feather
name="chevron-down"
style={{ marginLeft: "25%" }}
size={22}
color="black"
/>
}
>
{reminderOptions.map((option) => (
<Picker.Item
key={option.value}
label={option.label}
value={option.value}
/>
))}
</Picker>
</View>
</View>
<View style={styles.divider} />
<View style={styles.inner}>
<View style={styles.inputsStart}>
<Ionicons
name="repeat-sharp"
size={25}
color="black"
style={{ marginRight: 7 }}
/>
<Text text70>Auto Repeat</Text>
<Switch
style={{ marginLeft: "auto" }}
onColor={"#00c87e"}
offColor={"gray"}
value={autoRepeat}
onValueChange={(value) => setAutoRepeat(value)}
/>
</View>
</View>
<View style={styles.inner} marginT-10>
<View
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
width: "100%",
}}
>
<Button
label="Cancel"
style={styles.button}
color="black"
onPress={onClose}
/>
<Button
label="Save"
borderRadius={5}
backgroundColor="#00c87e"
style={{ width: "49%" }}
onPress={handleSave}
/>
</View>
</View>
</View>
</Dialog>
);
};
const styles = StyleSheet.create({
container: {
paddingVertical: 10,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
},
button: {
backgroundColor: "white",
color: "black",
borderColor: "lightgray",
borderWidth: 1,
borderRadius: 5,
padding: 0,
marginHorizontal: 0,
width: "49%",
},
inner: {
paddingHorizontal: 20,
display: "flex",
flexDirection: "column",
},
title: {
fontSize: 20,
fontWeight: "400",
textAlign: "center",
},
divider: {
width: "100%",
height: 1,
backgroundColor: "#E0E0E0",
marginVertical: 10,
},
iconTitle: {
padding: 10,
borderRadius: 10,
borderWidth: 1,
borderColor: "#E0E0E0",
display: "flex",
flexDirection: "row",
},
inputsBetween: {
paddingVertical: 12,
paddingHorizontal: 15,
marginVertical: 5,
borderRadius: 10,
borderWidth: 1,
borderColor: "#E0E0E0",
display: "flex",
alignItems: "center",
flexDirection: "row",
justifyContent: "space-between",
},
inputsStart: {
paddingVertical: 12,
paddingHorizontal: 15,
marginVertical: 5,
borderRadius: 10,
borderWidth: 1,
borderColor: "#E0E0E0",
display: "flex",
alignItems: "center",
flexDirection: "row",
justifyContent: "flex-start",
},
});
export default AddReminderModal;

View File

@ -0,0 +1,129 @@
import { useState } from "react";
import { StyleSheet } from "react-native";
import { Button, Card, Checkbox, Text, View } from "react-native-ui-lib";
import AddReminderModal from "./addReminderModal";
import ButtonOutlined from "@/components/ui/ButtonOutlined";
import AntDesign from "@expo/vector-icons/AntDesign";
import Feather from "@expo/vector-icons/Feather";
import { ScrollView } from "react-native-gesture-handler";
import { IReminder, useRemindersContext } from "@/contexts/RemindersContext";
const dateTimeDisplay = (date: Date) => {
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return (
<Text text80>
{`${day}/${month}/${year}`}{" "}
<AntDesign name="star" size={14} color="lightgray" />{" "}
{`${hours}:${minutes}`}
</Text>
);
};
const RemindersList = () => {
const {reminders, addReminder, updateReminder} = useRemindersContext();
const [isModalVisible, setIsModalVisible] = useState(false);
const handleModalClose = () => {
setIsModalVisible(false);
};
return (
<ScrollView
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
style={styles.listContainer}
>
<AddReminderModal
visible={isModalVisible}
onClose={handleModalClose}
onSave={addReminder}
/>
<View
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Text text70BL>Reminders</Text>
<ButtonOutlined
title={"New Reminder"}
size={Button.sizes.small}
onPress={() => setIsModalVisible(true)}
/>
</View>
<View style={styles.cardList}>
{reminders.map((reminder, index) => (
<Card key={index} style={{ marginBottom: 10, padding: 15 }}>
<View style={styles.card} row>
<View row>
<View style={styles.icon}>
<Feather name="scissors" size={24} color="black" />
</View>
<View>
<Text text70BL>{reminder.title}</Text>
{dateTimeDisplay(reminder.date)}
</View>
</View>
<Checkbox
size={30}
color="#f68d50"
value={reminder.done}
onValueChange={() => {
updateReminder(reminder.id, {done: !reminder.done})
}}
/>
</View>
</Card>
))}
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
header: {
flex: 1,
flexDirection: "row",
},
listContainer: {
marginLeft: "8%",
marginTop: "4%",
marginRight: "4%",
maxHeight: '71%'
},
button: {
backgroundColor: "white",
color: "black",
borderColor: "lightgray",
borderWidth: 1,
borderRadius: 5,
},
buttonText: {
margin: 0,
},
card: {
display: "flex",
justifyContent: "space-between",
},
cardList: {
marginTop: 10,
},
icon: {
borderRadius: 50,
height: 45,
width: 45,
backgroundColor: "#fcf4eb",
alignItems: "center",
justifyContent: "center",
marginRight: 10,
},
});
export default RemindersList;

View File

@ -0,0 +1,123 @@
import { AntDesign, Ionicons } from "@expo/vector-icons";
import React, { useState } from "react";
import { Button, View, Text, Checkbox } from "react-native-ui-lib";
import { StyleSheet } from "react-native";
import { colorMap } from "@/contexts/SettingsContext";
import { TouchableOpacity } from "react-native-gesture-handler";
const CalendarSettingsPage = (props: {
setSelectedPage: (page: number) => void;
}) => {
const [selectedColor, setSelectedColor] = useState<string>(colorMap.pink);
const [startDate, setStartDate] = useState<boolean>(true);
return (
<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>
);
};
const styles = StyleSheet.create({
backBtn: {
backgroundColor: "red",
marginLeft: -2,
justifyContent: "flex-start",
},
card: {
backgroundColor: "white",
width: "100%",
padding: 20,
paddingBottom: 30,
marginTop: 20,
borderRadius: 20,
},
colorBox: {
aspectRatio: 1,
justifyContent: "center",
alignItems: "center",
width: 50,
borderRadius: 12,
},
checkbox: {
borderRadius: 50,
},
});
export default CalendarSettingsPage;

View File

@ -0,0 +1,33 @@
import { View, Text, TouchableOpacity } from "react-native-ui-lib";
import React from "react";
import { Ionicons } from "@expo/vector-icons";
import { ToDosContextProvider } from "@/contexts/ToDosContext";
import ToDosList from "../todos/ToDosList";
import { ScrollView } from "react-native-gesture-handler";
const ChoreRewardSettings = (props: {
setSelectedPage: (page: number) => void;
}) => {
return (
<ToDosContextProvider>
<View marginT-10>
<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">
Return to main settings
</Text>
</View>
</TouchableOpacity>
<Text text60R marginL-30 marginB-20>
Chore Reward Settings
</Text>
<ToDosList />
</ScrollView>
</View>
</ToDosContextProvider>
);
};
export default ChoreRewardSettings;

View File

@ -0,0 +1,107 @@
import { View, Text, Button } from "react-native-ui-lib";
import React, { useState } from "react";
import { StyleSheet } from "react-native";
import { Entypo, Ionicons, Octicons } from "@expo/vector-icons";
import CalendarSettingsPage from "./CalendarSettingsPage";
import ChoreRewardSettings from "./ChoreRewardSettings";
import UserSettings from "./UserSettings";
import { AuthContextProvider } from "@/contexts/AuthContext";
const styles = StyleSheet.create({
mainBtn: {
width: "100%",
justifyContent: "flex-start",
marginBottom: 20,
height: 60,
},
});
const pageIndex = {
main: 0,
user: 1,
calendar: 2,
chore: 3,
policy: 4,
};
const SettingsPage = () => {
const [selectedPage, setSelectedPage] = useState<number>(0);
return (
<View>
{selectedPage == 0 && (
<View centerH marginH-30 marginT-30>
<Button
backgroundColor="white"
style={styles.mainBtn}
label="Manage my profile"
color="#07b8c7"
iconSource={() => (
<Ionicons
name="person-circle-sharp"
size={24}
color="#07b8c7"
style={{ marginRight: 10 }}
/>
)}
onPress={() => setSelectedPage(pageIndex.user)}
/>
<Button
backgroundColor="white"
style={styles.mainBtn}
label="Calendar settings"
color="#fd1775"
iconSource={() => (
<Ionicons
name="home-outline"
size={24}
color="#fd1775"
style={{ marginRight: 10 }}
/>
)}
onPress={() => {
setSelectedPage(pageIndex.calendar);
}}
/>
<Button
backgroundColor="white"
style={styles.mainBtn}
label="Chore reward settings"
color="#ff9900"
iconSource={() => (
<Octicons
name="gear"
size={24}
color="#ff9900"
style={{ marginRight: 10 }}
/>
)}
onPress={() => setSelectedPage(pageIndex.chore)}
/>
<Button
backgroundColor="white"
style={styles.mainBtn}
label="Kaly privacy policy"
iconSource={() => (
<Entypo
name="text-document"
size={24}
color="#6c645b"
style={{ marginRight: 10 }}
/>
)}
color="#6c645b"
/>
</View>
)}
{selectedPage == pageIndex.calendar && (
<CalendarSettingsPage setSelectedPage={setSelectedPage} />
)}
{selectedPage == pageIndex.chore && (
<ChoreRewardSettings setSelectedPage={setSelectedPage} />
)}
{selectedPage == pageIndex.user && (
<UserSettings setSelectedPage={setSelectedPage} />
)}
</View>
);
};
export default SettingsPage;

View File

@ -0,0 +1,80 @@
import { View, Text, TouchableOpacity } from "react-native-ui-lib";
import React, { useState } from "react";
import { Ionicons } from "@expo/vector-icons";
import { ScrollView, StyleSheet } from "react-native";
import MyProfile from "./user_settings_views/MyProfile";
import MyGroup from "./user_settings_views/MyGroup";
import { useAuthContext } from "@/contexts/AuthContext";
const UserSettings = (props: { setSelectedPage: (page: number) => void }) => {
const [selectedView, setSelectedView] = useState<boolean>(true);
return (
<View>
<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">
Return to main settings
</Text>
</View>
</TouchableOpacity>
<View marginH-20>
<Text text60R marginB-25>
User Management
</Text>
<View style={styles.buttonSwitch} spread row>
<TouchableOpacity
onPress={() => setSelectedView(true)}
centerV
centerH
style={selectedView == true ? styles.btnSelected : styles.btnNot}
>
<View>
<Text text70 color={selectedView ? "white" : "black"}>
My Profile
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setSelectedView(false)}
centerV
centerH
style={selectedView == false ? styles.btnSelected : styles.btnNot}
>
<View>
<Text text70 color={!selectedView ? "white" : "black"}>
My Group
</Text>
</View>
</TouchableOpacity>
</View>
{selectedView && <MyProfile />}
{!selectedView && <MyGroup />}
</View>
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
buttonSwitch: {
borderRadius: 50,
width: "100%",
backgroundColor: "#ebebeb",
height: 45,
},
btnSelected: {
backgroundColor: "#05a8b6",
height: "100%",
width: "50%",
borderRadius: 50,
},
btnNot: {
height: "100%",
width: "50%",
borderRadius: 50,
},
});
export default UserSettings;

View File

@ -0,0 +1,12 @@
import { View, Text } from "react-native";
import React from "react";
const GroupsList = () => {
return (
<View>
<Text>GroupsList</Text>
</View>
);
};
export default GroupsList;

View File

@ -0,0 +1,43 @@
import { View, Text } from "react-native-ui-lib";
import React from "react";
import { StyleSheet } from "react-native";
const MyGroup = () => {
return (
<View>
<View style={styles.card}>
<Text text70 marginV-10>
Family
</Text>
<Text text70 marginB-10 marginT-15>
Caregivers
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
card: {
marginVertical: 15,
backgroundColor: "white",
width: "100%",
borderRadius: 15,
padding: 20,
},
pfp: {
aspectRatio: 1,
width: 60,
backgroundColor: "green",
borderRadius: 20,
},
txtBox: {
backgroundColor: "#fafafa",
borderRadius: 50,
borderWidth: 2,
borderColor: "#cecece",
padding: 15,
height: 45,
},
});
export default MyGroup;

View File

@ -0,0 +1,102 @@
import { View, Text, TextField } from "react-native-ui-lib";
import React, { useState } from "react";
import { 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();
const [lastName, setLastName] = useState<string>(profileData?.lastName || "");
const [firstName, setFirstName] = useState<string>(
profileData?.firstName || ""
);
const { mutateAsync: updateUserData } = useUpdateUserData();
return (
<View>
<View style={styles.card}>
<Text text70>Your Profile</Text>
<View row spread paddingH-15 centerV marginV-15>
<View style={styles.pfp}></View>
<Text text80 color="#50be0c">
Change Photo
</Text>
<Text text80>Remove Photo</Text>
</View>
<View paddingH-15>
<Text text80 marginT-10 marginB-7 color="#a1a1a1">
First name
</Text>
<TextField
text70
placeholder="First name"
style={styles.txtBox}
value={firstName}
onChangeText={async (value) => {
setFirstName(value);
await updateUserData({ newUserData: { firstName: value } });
}}
/>
<Text text80 marginT-10 marginB-7 color="#a1a1a1">
Last name
</Text>
<TextField
text70
placeholder="Last name"
style={styles.txtBox}
value={lastName}
onChangeText={async (value) => {
setLastName(value);
await updateUserData({ newUserData: { lastName: value } });
}}
/>
<Text text80 marginT-10 marginB-7 color="#a1a1a1">
Email address
</Text>
<TextField
text70
placeholder="Email address"
value={user?.email?.toString()}
style={styles.txtBox}
/>
</View>
</View>
<View style={styles.card}>
<Text text70>Settings</Text>
<Text text80 marginT-20 marginB-7 color="#a1a1a1">
Time Zone
</Text>
<TextField text70 placeholder="Time Zone" style={styles.txtBox} />
</View>
</View>
);
};
const styles = StyleSheet.create({
card: {
marginVertical: 15,
backgroundColor: "white",
width: "100%",
borderRadius: 15,
padding: 20,
},
pfp: {
aspectRatio: 1,
width: 60,
backgroundColor: "green",
borderRadius: 20,
},
txtBox: {
backgroundColor: "#fafafa",
borderRadius: 50,
borderWidth: 2,
borderColor: "#cecece",
padding: 15,
height: 45,
},
});
export default MyProfile;

View File

@ -0,0 +1,274 @@
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 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";
const AddChore = () => {
const { addToDo, toDos } = useToDosContext();
const [isVisible, setIsVisible] = useState<boolean>(false);
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 handleChange = (text: string) => {
const numericValue = parseInt(text, 10);
if (!isNaN(numericValue) && numericValue >= 0 && numericValue <= 100) {
setPoints(numericValue);
} else if (text === "") {
setPoints(0);
}
};
return (
<LinearGradient
colors={["transparent", "#f9f8f7"]}
locations={[0, 0.5]}
style={styles.gradient}
>
<View style={styles.buttonContainer}>
<Button
marginH-25
size={ButtonSize.large}
style={styles.button}
onPress={() => setIsVisible(!isVisible)}
>
<AntDesign name="plus" size={24} color="white" />
<Text white text60R marginL-10>
Create new chore
</Text>
</Button>
</View>
<Dialog
bottom={true}
height={"90%"}
panDirection={PanningDirectionsEnum.DOWN}
onDismiss={() => setIsVisible(false)}
containerStyle={{
borderRadius: 10,
backgroundColor: "white",
width: "100%",
alignSelf: "stretch",
padding: 0,
paddingTop: 3,
margin: 0,
}}
visible={isVisible}
>
<View row spread>
<Button
color="#05a8b6"
style={styles.topBtn}
label="Cancel"
onPress={() => {
setIsVisible(false);
}}
/>
<Button
style={styles.topBtn}
iconSource={() => (
<Feather name="chevron-down" size={24} color="black" />
)}
onPress={() => {
setIsVisible(false);
}}
/>
<Button
color="#05a8b6"
style={styles.topBtn}
label="Save"
onPress={() => {
addToDo({
id: 0,
title: newTitle,
done: false,
date: choreDate,
points: points,
rotate: rotate,
repeatType: repeatType,
});
setIsVisible(false);
console.log(toDos);
}}
/>
</View>
<TextField
placeholder="Add a To Do"
value={newTitle}
onChangeText={(text) => {
setNewTitle(text);
}}
placeholderTextColor="#2d2d30"
text60R
marginT-15
marginL-30
/>
<View style={styles.divider} marginT-8 />
<View marginL-30 centerV>
<View row marginB-10>
{choreDate && (
<View row centerV>
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={choreDate}
text70
marginL-8
onChange={(date) => {
setChoreDate(date);
}}
/>
</View>
)}
</View>
<View row centerV>
<AntDesign name="clockcircleo" size={24} color="#919191" />
<Picker
marginL-8
placeholder="Select Repeat Type"
value={repeatType}
onChange={(value) => {
if (value) {
if (value.toString() == "None") {
setChoreDate(null);
setRepeatType("None");
} else {
setRepeatType(value.toString());
setChoreDate(new Date());
}
}
}}
topBarProps={{ title: "Repeat" }}
style={{ marginVertical: 5 }}
>
{repeatOptions.map((option) => (
<Picker.Item
key={option.value}
label={option.label}
value={option.value}
/>
))}
</Picker>
</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>
<Button
size={ButtonSize.small}
paddingH-8
iconSource={() => (
<Ionicons name="add-outline" size={20} color="#ea156c" />
)}
style={{
marginLeft: "auto",
borderRadius: 8,
backgroundColor: "#ffe8f1",
borderColor: "#ea156c",
borderWidth: 1,
}}
color="#ea156c"
label="Assign"
/>
</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>
<View row centerV style={styles.rotateSwitch}>
<Text text80>Take Turns</Text>
<Switch
onColor={"#ea156c"}
value={rotate}
marginL-10
onValueChange={(value) => setRotate(value)}
/>
</View>
<View style={styles.divider} />
<View marginH-30 marginB-15 row centerV>
<Ionicons name="gift-outline" size={25} color="#919191" />
<Text text70BL marginL-10>
Reward Points
</Text>
</View>
<PointsSlider
points={points}
setPoints={setPoints}
handleChange={handleChange}
/>
</Dialog>
</LinearGradient>
);
};
export default AddChore;
const styles = StyleSheet.create({
divider: { height: 1, backgroundColor: "#e4e4e4", marginVertical: 15 },
gradient: {
height: "25%",
position: "absolute",
bottom: 0,
width: "100%",
},
buttonContainer: {
position: "absolute",
bottom: 25,
width: "100%",
},
button: {
backgroundColor: "rgb(253, 23, 117)",
paddingVertical: 20,
},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
rotateSwitch: {
marginLeft: 35,
marginBottom: 10,
marginTop: 25,
},
});

View File

@ -0,0 +1,48 @@
import { View, Text, Button } from "react-native-ui-lib";
import React from "react";
import { Fontisto } from "@expo/vector-icons";
import { ProgressBar } from "react-native-ui-lib/src/components/progressBar";
const ProgressCard = () => {
return (
<View
backgroundColor="white"
marginH-25
marginB-30
padding-15
style={{ borderRadius: 22 }}
>
<View row centerV>
<Fontisto name="day-sunny" size={30} color="#ff9900" />
<Text marginL-5 text70>
You have earned XX points this week!{" "}
</Text>
</View>
<ProgressBar
progress={50}
progressColor="#ea156c"
style={{
height: 21,
backgroundColor: "#fcf2f6",
marginTop: 15,
marginBottom: 5,
}}
/>
<View row spread>
<Text color={"#868686"}>0</Text>
<Text color={"#868686"}>1000</Text>
</View>
<View centerV centerH>
<Button
backgroundColor="transparent"
>
<Text style={{ textDecorationLine: "underline", color: "#05a8b6" }}>
View your full progress report here
</Text>
</Button>
</View>
</View>
);
};
export default ProgressCard;

View File

@ -0,0 +1,68 @@
import { View, Text, Checkbox } from "react-native-ui-lib";
import React from "react";
import { IToDo, useToDosContext } from "@/contexts/ToDosContext";
import { Ionicons } from "@expo/vector-icons";
const ToDoItem = (props: { item: IToDo }) => {
const { updateToDo } = useToDosContext();
return (
<View
centerV
paddingV-10
paddingH-10
marginH-25
marginV-10
style={{
borderRadius: 22,
backgroundColor: props.item.done ? "#e0e0e0" : "white",
opacity: props.item.done ? 0.3 : 1,
}}
>
<View paddingB-5 row spread>
<Text
text70R
style={{
textDecorationLine: props.item.done ? "line-through" : "none",
}}
>
{props.item.title}
</Text>
<Checkbox
value={props.item.done}
onValueChange={(value) => {
updateToDo(props.item.id, { done: !props.item.done });
}}
/>
</View>
<View centerH paddingV-5>
<View
centerV
height={2}
width={"100%"}
style={{
backgroundColor: props.item.done ? '#b8b8b8' : "#e7e7e7",
}}
centerH
/>
</View>
<View centerH 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>
) : (
<View />
)}
<View
height={25}
width={25}
backgroundColor="red"
style={{ borderRadius: 50 }}
/>
</View>
</View>
);
};
export default ToDoItem;

View File

@ -0,0 +1,78 @@
import { View, Text } from "react-native-ui-lib";
import React from "react";
import { IToDo, useToDosContext } from "@/contexts/ToDosContext";
import ToDoItem from "./ToDoItem";
import { format, isToday, isTomorrow } from "date-fns";
const groupToDosByDate = (toDos: IToDo[]) => {
return toDos.reduce((groups, toDo) => {
let dateKey;
if (toDo.date === null) {
dateKey = "No Date";
} else if (isToday(toDo.date)) {
dateKey = "Today • " + format(toDo.date, "EEE MMM dd");
} else if (isTomorrow(toDo.date)) {
dateKey = "Tomorrow • " + format(toDo.date, "EEE MMM dd");
} else {
dateKey = format(toDo.date, "EEE MMM dd");
}
if (!groups[dateKey]) {
groups[dateKey] = [];
}
groups[dateKey].push(toDo);
return groups;
}, {} as { [key: string]: IToDo[] });
};
const ToDosList = () => {
const { toDos } = useToDosContext();
const groupedToDos = groupToDosByDate(toDos);
const noDateToDos = groupedToDos["No Date"] || [];
const datedToDos = Object.keys(groupedToDos).filter((key) => key !== "No Date");
return (
<View marginB-140>
{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>
)}
{datedToDos.map((dateKey) => {
const sortedToDos = [
...groupedToDos[dateKey].filter((toDo) => !toDo.done),
...groupedToDos[dateKey].filter((toDo) => toDo.done),
];
return (
<View key={dateKey}>
<Text
text70
style={{
fontWeight: "bold",
marginVertical: 8,
paddingHorizontal: 20,
}}
>
{dateKey}
</Text>
{sortedToDos.map((item) => (
<ToDoItem key={item.id} item={item} />
))}
</View>
);
})}
</View>
);
};
export default ToDosList;

View File

@ -0,0 +1,51 @@
import React from "react";
import { Button, View, Text } from "react-native-ui-lib";
interface IDrawerButtonProps {
bgColor: string;
color: string;
pressFunc: () => void;
icon: React.ReactNode;
title: string;
}
const DrawerButton = (props: IDrawerButtonProps) => {
return (
<Button
onPress={props.pressFunc}
label={props.title}
labelStyle={{ fontSize: 14 }}
color={props.color}
backgroundColor="white"
iconSource={() => (
<View
backgroundColor={props.bgColor}
width={60}
height={60}
style={{ borderRadius: 50 }}
centerV
centerH
>
{props.icon}
</View>
)}
style={{
aspectRatio: 1,
borderRadius: 15,
marginBottom: 12,
flexDirection: "column",
justifyContent: "space-between",
paddingVertical: 15,
// Shadow for iOS
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 10, // This will create a blurry shadow
// Shadow for Android
elevation: 1,
}}
></Button>
);
};
export default DrawerButton;

View File

@ -0,0 +1,25 @@
import { View, Text } from "react-native-ui-lib";
import React from "react";
import { useAuthContext } from "@/contexts/AuthContext";
const HeaderTemplate = (props: { message: string; isWelcome: boolean; children?: React.ReactNode }) => {
const { user, profileData } = useAuthContext();
return (
<View row centerV padding-25>
<View
backgroundColor="pink"
height={65}
width={65}
style={{ borderRadius: 22 }}
marginR-20
/>
<View>
{props.isWelcome && <Text text70L>Welcome, {user?.email}!</Text>}
<Text text70BL>{props.message}</Text>
{props.children && <View>{props.children}</View>}
</View>
</View>
);
};
export default HeaderTemplate;

View File

@ -0,0 +1,43 @@
import { View, Text, Slider, TextField } from "react-native-ui-lib";
import React from "react";
const PointsSlider = (props: {
points: number;
setPoints(value: number): void;
handleChange(value: string): void;
}) => {
return (
<View marginH-30 row spread>
<View width={"80%"}>
<Slider
value={props.points}
onValueChange={(value) => props.setPoints(value)}
minimumValue={0}
step={10}
maximumValue={100}
/>
<View row spread>
<Text>0</Text>
<Text>50</Text>
<Text>100</Text>
</View>
</View>
<View style={{ marginLeft: "auto" }}>
<TextField
value={props.points.toString()}
onChangeText={(text) => {
props.handleChange(text);
}}
containerStyle={{
borderWidth: 1,
borderColor: "#d9d9d9",
width: 45,
borderRadius: 5,
}}
/>
</View>
</View>
);
};
export default PointsSlider;

View File

@ -0,0 +1,34 @@
import { StyleSheet, View } from "react-native";
import React from "react";
import { Text, Button } from "react-native-ui-lib";
interface ButtonProps {
title: string;
size: keyof typeof Button.sizes;
onPress: () => void;
}
const ButtonOutlined = (props: ButtonProps) => {
const { size, onPress, title } = props;
return (
<Button style={styles.button} size={size} onPress={onPress} avoidMinWidth>
<Text text80L style={styles.buttonText}>
{title}
</Text>
</Button>
);
};
export default ButtonOutlined;
const styles = StyleSheet.create({
button: {
backgroundColor: "white",
color: "black",
borderColor: "lightgray",
borderWidth: 1,
borderRadius: 5,
padding: 0,
marginHorizontal: 0
},
buttonText: {
},
});

87
contexts/AuthContext.tsx Normal file
View File

@ -0,0 +1,87 @@
import {createContext, FC, ReactNode, useContext, useEffect, useState} from "react";
import * as SplashScreen from "expo-splash-screen";
import auth, {FirebaseAuthTypes} from "@react-native-firebase/auth";
import {useRouter} from "expo-router";
import firestore from "@react-native-firebase/firestore";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
export enum ProfileType {
"PARENT" = "parent",
"CHILD" = "child",
"CAREGIVER" = "caregiver"
}
interface IAuthContext {
user: FirebaseAuthTypes.User | null,
profileType?: ProfileType,
profileData?: UserProfile
setProfileData: (profileData: UserProfile) => void
}
const AuthContext = createContext<IAuthContext>(undefined!)
export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) => {
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 onAuthStateChanged = async (user: FirebaseAuthTypes.User | null) => {
setUser(user);
console.log(user)
if (user) {
try {
const documentSnapshot = await firestore()
.collection("Profiles")
.doc(user.uid)
.get();
if (documentSnapshot.exists) {
setProfileType(documentSnapshot.data()?.userType);
setProfileData(documentSnapshot.data() as UserProfile)
}
} catch (error) {
console.error("Error fetching user profile type:", error);
setProfileType(undefined);
}
}
if (initializing) setInitializing(false);
}
useEffect(() => {
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
return subscriber;
}, []);
useEffect(() => {
if (!initializing) {
SplashScreen.hideAsync();
}
}, [initializing]);
useEffect(() => {
if (ready && user) {
replace({pathname: "/(auth)/calendar"})
} else if (ready && !user) {
replace({pathname: "/(unauth)"})
}
}, [user, ready]);
if (!ready) {
return null;
}
return (
<AuthContext.Provider value={{user, profileType, profileData, setProfileData}}>
{children}
</AuthContext.Provider>
)
}
export const useAuthContext = () => useContext(AuthContext)!;

100
contexts/DumpContext.tsx Normal file
View File

@ -0,0 +1,100 @@
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { createContext, useContext, useState } from "react";
export interface IBrainDump {
id: number;
title: string;
description: string;
}
interface IBrainDumpContext {
brainDumps: IBrainDump[];
updateBrainDumpItem: (id: number, changes: Partial<IBrainDump>) => void;
isAddingBrainDump: boolean;
setIsAddingBrainDump: (value: boolean) => void;
addBrainDump: (BrainDump: IBrainDump) => void;
}
const BrainDumpContext = createContext<IBrainDumpContext | undefined>(
undefined
);
export const BrainDumpProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [isAddingBrainDump, setIsAddingBrainDump] = useState<boolean>(false);
const [brainDumps, setBrainDumps] = useState<IBrainDump[]>([
{
id: 0,
title: "Favorite Weekend Activities",
description:
"What's something fun we can do together this weekend? Maybe a new game, a picnic, or trying out a family recipe. Everyone share one idea!",
},
{
id: 1,
title: "Whats For Dinner",
description:
"Whats one meal youd love to have for dinner this week? Lets get creative with new ideas we havent tried yet.",
},
{
id: 2,
title: "The Best Thing About Today",
description:
"What was the highlight of your day? Lets each take a moment to share something good that happened, no matter how small.",
},
{
id: 3,
title: "A Dream Vacation Spot",
description:
"If we could go anywhere in the world right now, where would it be? Everyone pick one dream destination and tell us why.",
},
{
id: 4,
title: "Favorite Childhood Memory",
description:
"Whats a favorite memory from your childhood? Lets take a trip down memory lane and share some of the moments that made us smile.",
},
{
id: 5,
title: "A New Family Tradition",
description:
"Whats one new tradition we could start as a family? Maybe a weekly movie night, a monthly game day, or a yearly family trip. Share your ideas!",
},
]);
const addBrainDump = (BrainDump: IBrainDump) => {
setBrainDumps((prevBrainDumps) => [
...prevBrainDumps,
{
...BrainDump,
id: prevBrainDumps.length
? prevBrainDumps[prevBrainDumps.length - 1].id + 1
: 0,
},
]);
};
const updateBrainDumpItem = (id: number, changes: Partial<IBrainDump>) => {
setBrainDumps((prevBrainDumps) =>
prevBrainDumps.map((BrainDump) =>
BrainDump.id === id ? { ...BrainDump, ...changes } : BrainDump
)
);
};
return (
<BrainDumpContext.Provider
value={{
brainDumps,
updateBrainDumpItem,
isAddingBrainDump,
setIsAddingBrainDump,
addBrainDump,
}}
>
{children}
</BrainDumpContext.Provider>
);
};
export const useBrainDumpContext = () => useContext(BrainDumpContext)!;

135
contexts/GroceryContext.tsx Normal file
View File

@ -0,0 +1,135 @@
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { createContext, useContext, useState } from "react";
export enum GroceryFrequency {
Never = "Never",
Daily = "Daily",
Weekly = "Weekly",
BiWeekly = "BiWeekly",
Monthly = "Monthly",
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",
Vegetables = "Vegetables",
Meat = "Meat",
Poultry = "Poultry",
Bakery = "Bakery",
Beverages = "Beverages",
Snacks = "Snacks",
Household = "Household",
PersonalCare = "Personal Care",
Frozen = "Frozen",
}
type MaterialIconNames = keyof typeof MaterialCommunityIcons.glyphMap;
const iconMapping: { [key in GroceryCategory]: MaterialIconNames } = {
//за сад се иконице за категорију бирају одавде
[GroceryCategory.Fruit]: "food-apple",
[GroceryCategory.Dairy]: "cheese",
[GroceryCategory.Vegetables]: "carrot",
[GroceryCategory.Meat]: "food-steak",
[GroceryCategory.Poultry]: "food-drumstick",
[GroceryCategory.Bakery]: "bread-slice",
[GroceryCategory.Beverages]: "cup-water",
[GroceryCategory.Snacks]: "candy",
[GroceryCategory.Household]: "home",
[GroceryCategory.PersonalCare]: "face-man-profile",
[GroceryCategory.Frozen]: "snowflake",
};
interface IGroceryContext {
groceries: IGrocery[];
iconMapping: { [key in GroceryCategory]: MaterialIconNames };
updateGroceryItem: (id: number, changes: Partial<IGrocery>) => void;
isAddingGrocery: boolean;
setIsAddingGrocery: (value: boolean) => void;
addGrocery: (grocery: IGrocery) => void;
}
const GroceryContext = createContext<IGroceryContext | undefined>(undefined);
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 addGrocery = (grocery: IGrocery) => {
setGroceries((prevGroceries) => [
...prevGroceries,
{
...grocery,
id: prevGroceries.length ? prevGroceries[prevGroceries.length - 1].id + 1 : 0,
},
]);
};
const updateGroceryItem = (id: number, changes: Partial<IGrocery>) => {
setGroceries((prevGroceries) =>
prevGroceries.map((grocery) =>
grocery.id === id ? { ...grocery, ...changes } : grocery
)
);
};
return (
<GroceryContext.Provider
value={{ groceries, iconMapping, updateGroceryItem, isAddingGrocery, setIsAddingGrocery, addGrocery }}
>
{children}
</GroceryContext.Provider>
);
};
export const useGroceryContext = () => useContext(GroceryContext)!;

View File

@ -0,0 +1,67 @@
import { View, Text } from "react-native";
import React, { createContext, useContext, useState } from "react";
export interface IReminder {
id: number;
title: string;
date: Date;
done: boolean;
isAutoRepeat: boolean;
remindIn: string;
}
interface IRemindersContext {
reminders: IReminder[];
updateReminder: (id: number, changes: Partial<IReminder>) => void;
addReminder: (changes: Omit<IReminder, "id">) => void;
}
const RemindersContext = createContext<IRemindersContext | undefined>(
undefined
);
export const RemindersProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [reminders, setReminders] = useState<IReminder[]>([
{
id: 0,
title: "Shaving Time",
date: new Date(2023, 1, 30, 9, 30),
done: true,
isAutoRepeat: true,
remindIn: "just-in-time",
},
{
id: 1,
title: "Gonna get a food order",
date: new Date(2023, 1, 27),
done: false,
isAutoRepeat: true,
remindIn: "just-in-time",
},
]);
const updateReminder = (id: number, changes: Partial<IReminder>) => {
setReminders((prevReminder) =>
prevReminder.map((reminder) =>
reminder.id === id ? { ...reminder, ...changes } : reminder
)
);
};
const addReminder = (newReminder: Omit<IReminder, "id">) => {
const newId =
reminders.length > 0 ? reminders[reminders.length - 1].id + 1 : 1;
const reminderWithId: IReminder = {
...newReminder,
id: newId,
};
setReminders([...reminders, reminderWithId]);
};
return (
<RemindersContext.Provider
value={{ reminders, updateReminder, addReminder }}
>
{children}
</RemindersContext.Provider>
);
};
export const useRemindersContext = () => useContext(RemindersContext)!;

View File

@ -0,0 +1,53 @@
import { createContext, FC, ReactNode, useContext, useState } from "react";
import { useAuthContext } from "./AuthContext";
export const colorMap = {
pink: "#ea156c",
orange: "#e28800",
green: "#46a80a",
teal: "#05a8b6",
purple: "#7305d4",
};
interface IUserDetails {
email: string | undefined;
firstName: string;
lastName: string;
}
interface ISettingsContext {
calendarColor: string;
userDetails: IUserDetails;
editUserDetails: (details: Partial<IUserDetails>) => void;
setCalendarColor: (color: string) => void;
}
const SettingsContext = createContext<ISettingsContext>(undefined!);
export const SettingsContextProvider: FC<{ children: ReactNode }> = ({
children,
}) => {
const { user } = useAuthContext();
const [userDetails, setUserDetails] = useState<IUserDetails>({
email: user?.email?.toString(),
firstName: "",
lastName: "",
});
const [calendarColor, setCalendarColor] = useState<string>(colorMap.pink);
const editUserDetails = (details: Partial<IUserDetails>) => {
setUserDetails((prevDetails) => ({
...prevDetails,
...details,
}));
};
return (
<SettingsContext.Provider
value={{ calendarColor, setCalendarColor, userDetails, editUserDetails }}
>
{children}
</SettingsContext.Provider>
);
};
export const useSettingsContext = () => useContext(SettingsContext)!;

130
contexts/ToDosContext.tsx Normal file
View File

@ -0,0 +1,130 @@
import { createContext, FC, ReactNode, useContext, useState } from "react";
export interface IToDo {
id: number;
title: string;
done: boolean;
date: Date | null;
points?: number;
rotate: boolean;
repeatType: string;
}
export const repeatOptions = [
{ label: "None", value: "None" },
{ label: "Every week", value: "Every week" },
{ label: "Once a month", value: "Once a month" },
{ label: "Once a year", value: "Once a year" },
];
interface IToDosContext {
toDos: IToDo[];
updateToDo: (id: number, changes: Partial<IToDo>) => void;
addToDo: (newToDo: IToDo) => void;
}
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"
},
{
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"
},
]);
const updateToDo = (id: number, changes: Partial<IToDo>) => {
setToDos((prevToDos) =>
prevToDos.map((toDo) => (toDo.id === id ? { ...toDo, ...changes } : toDo))
);
};
const addToDo = (newToDo: IToDo) => {
setToDos((prevToDos) => [
...prevToDos,
{
...newToDo,
id: prevToDos.length ? prevToDos[prevToDos.length - 1].id + 1 : 0,
},
]);
};
return (
<ToDosContext.Provider value={{ toDos, updateToDo, addToDo }}>
{children}
</ToDosContext.Provider>
);
};
export const useToDosContext = () => useContext(ToDosContext)!;

5
firebase/.firebaserc Normal file
View File

@ -0,0 +1,5 @@
{
"projects": {
"default": "cally-family-calendar"
}
}

69
firebase/.gitignore vendored Normal file
View File

@ -0,0 +1,69 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*
firebase-debug.*.log*
# Firebase cache
.firebase/
# Firebase config
# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# dataconnect generated files
.dataconnect

37
firebase/firebase.json Normal file
View File

@ -0,0 +1,37 @@
{
"functions": [
{
"source": "functions",
"codebase": "default",
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log",
"*.local"
],
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint"
]
}
],
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"emulators": {
"auth": {
"port": 9099
},
"functions": {
"port": 5001
},
"firestore": {
"port": 5471
},
"ui": {
"enabled": true
},
"singleProjectMode": true
}
}

View File

@ -0,0 +1,4 @@
{
"indexes": [],
"fieldOverrides": []
}

19
firebase/firestore.rules Normal file
View File

@ -0,0 +1,19 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// This rule allows anyone with your Firestore database reference to view, edit,
// and delete all data in your Firestore database. It is useful for getting
// started, but it is configured to expire after 30 days because it
// leaves your app open to attackers. At that time, all client
// requests to your Firestore database will be denied.
//
// Make sure to write security rules for your app before that time, or else
// all client requests to your Firestore database will be denied until you Update
// your rules
match /{document=**} {
allow read, write;
}
}
}

View File

@ -0,0 +1,28 @@
module.exports = {
env: {
es6: true,
node: true,
},
parserOptions: {
"ecmaVersion": 2018,
},
extends: [
"eslint:recommended",
"google",
],
rules: {
"no-restricted-globals": ["error", "name", "length"],
"prefer-arrow-callback": "error",
"quotes": ["error", "double", {"allowTemplateLiterals": true}],
},
overrides: [
{
files: ["**/*.spec.*"],
env: {
mocha: true,
},
rules: {},
},
],
globals: {},
};

2
firebase/functions/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
*.local

View File

@ -0,0 +1,45 @@
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");
admin.initializeApp();
exports.createSubUser = onRequest(async (request, response) => {
try {
logger.info("Processing user creation", {requestBody: request.body.data});
const {userType, name, email, password} = request.body.data;
if (!email || !password || !name || !userType) {
throw new Error("Missing required fields");
}
const userRecord = await getAuth().createUser({
email,
password,
displayName: name,
});
const userProfile = {
userType,
name,
email,
uid: userRecord.uid,
};
await getFirestore().collection("Profiles").doc(userRecord.uid).set(userProfile);
response.status(200).json({
data: {
message: "User created successfully",
userId: userRecord.uid,
}
});
} catch (error) {
logger.error("Error in createSubUser function", {error: error.message});
response.status(500).json({data: {error: error.message}});
}
});

7146
firebase/functions/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"lint": "eslint .",
"serve": "firebase emulators:start --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "18"
},
"main": "index.js",
"dependencies": {
"firebase-admin": "^12.1.0",
"firebase-functions": "^5.0.0"
},
"devDependencies": {
"eslint": "^8.15.0",
"eslint-config-google": "^0.14.0",
"firebase-functions-test": "^3.1.0"
},
"private": true
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,13 @@
export interface EventData {
title: string,
startDate: Date,
endDate: Date,
allDay: boolean,
repeatDays: string[],
creatorId: string[],
userIds: string[],
timeZone?: string,
surpriseEvent?: boolean,
notes?: string,
reminders?: string[]
}

View File

@ -0,0 +1,34 @@
import {ProfileType} from "@/contexts/AuthContext";
export interface User {
uid: string;
email: string | null;
}
export interface UserProfile {
userType: ProfileType;
firstName: string;
lastName: string;
childrenIds?: string[];
birthday?: Date;
parentId?: string;
contact?: string;
email: string
password: string
}
export interface ParentProfile extends UserProfile {
userType: ProfileType.PARENT;
childrenIds: string[];
}
export interface ChildProfile extends UserProfile {
userType: ProfileType.CHILD;
birthday: Date;
parentId: string;
}
export interface CaregiverProfile extends UserProfile {
userType: ProfileType.CAREGIVER;
contact: string;
}

View File

@ -0,0 +1,25 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation, useQueryClient} from "react-query";
import firestore from "@react-native-firebase/firestore";
import {EventData} from "@/hooks/firebase/types/eventData";
export const useCreateEvent = () => {
const {user: currentUser} = useAuthContext()
const queryClients = useQueryClient()
return useMutation({
mutationKey: ["createEvent"],
mutationFn: async (eventData: Partial<EventData>) => {
try {
await firestore()
.collection("Events")
.add({...eventData, creatorId: currentUser?.uid})
} catch (e) {
console.error(e)
}
},
onSuccess: () => {
queryClients.invalidateQueries("events")
}
})
}

View File

@ -0,0 +1,23 @@
import {useMutation, useQueryClient} from "react-query";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
import functions from '@react-native-firebase/functions';
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
export const useCreateSubUser = () => {
const queryClient = useQueryClient()
const { profileType } = useAuthContext()
return useMutation({
mutationKey: ["createSubUser"],
mutationFn: async ({email, ...userProfile}: { email: string } & UserProfile) => {
if(profileType === ProfileType.PARENT) {
return await functions().httpsCallable("createSubUser")({email, ...userProfile})
} else {
throw Error("Can't create sub-users as a non-parent.")
}
},
onSuccess: () => {
queryClient.invalidateQueries({queryKey: ["getChildrenByParentId"]})
}
});
}

View File

@ -0,0 +1,14 @@
import { useMutation } from "react-query";
import firestore from "@react-native-firebase/firestore";
export const useSignUp = () => {
return useMutation({
mutationKey: ["getCaregivers"],
mutationFn: async () => {
const snapshot = await firestore()
.collection("Profiles")
.where("userType", "==", "caregiver")
.get();
},
});
};

View File

@ -0,0 +1,34 @@
import {useQuery} from "react-query";
import {ChildProfile} from "@/hooks/firebase/types/profileTypes";
import firestore from "@react-native-firebase/firestore";
import {useAuthContext} from "@/contexts/AuthContext";
export const useGetChildrenByParentId = () => {
const {user} = useAuthContext()
return useQuery({
queryKey: ["getChildrenByParentId", user?.uid],
queryFn: async (): Promise<ChildProfile[]> => {
try {
const snapshot = await firestore()
.collection("Profiles")
.where("userType", "==", "child")
.where("parentId", "==", user?.uid!)
.get();
return snapshot.docs.map((doc) => {
const data = doc.data();
return {
...data,
birthday: data.birthday.toDate(),
} as ChildProfile;
});
} catch (error) {
console.error("Error retrieving child users:", error);
return [];
}
},
enabled: !!user?.uid
}
)
}

View File

@ -0,0 +1,31 @@
import {useQuery} from "react-query";
import firestore from "@react-native-firebase/firestore";
import {ReactElement} from "react";
import {useAuthContext} from "@/contexts/AuthContext";
import {ICalendarEventBase} from "react-native-big-calendar";
export const useGetEvents = () => {
const {user} = useAuthContext()
return useQuery({
queryKey: ["events", user?.uid],
queryFn: async () => {
const snapshot = await firestore()
.collection("Events")
.where("creatorId", "==", user?.uid)
.get();
const events: ICalendarEventBase[] = snapshot.docs.map((doc) => {
const data = doc.data();
return {
title: data.title,
start: new Date(data.startDate.seconds * 1000),
end: new Date(data.endDate.seconds * 1000),
hideHours: data.allDay,
};
});
return events;
}
})
}

View File

@ -0,0 +1,18 @@
import { useMutation } from "react-query";
import auth from "@react-native-firebase/auth";
export const useSignIn = () => {
return useMutation({
mutationKey: ["signIn"],
mutationFn: async ({
email,
password,
}: {
email: string;
password: string;
}) => {
await auth().signInWithEmailAndPassword(email, password);
},
});
};

View File

@ -0,0 +1,11 @@
import {useMutation} from "react-query";
import auth from "@react-native-firebase/auth";
export const useSignOut = () => {
return useMutation({
mutationKey: ["signOut"],
mutationFn: async () => {
await auth().signOut()
}
});
}

View File

@ -0,0 +1,16 @@
import {useMutation} from "react-query";
import auth from "@react-native-firebase/auth";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
import {ProfileType} from "@/contexts/AuthContext";
export const useSignUp = () => {
const {mutateAsync: updateUserData} = useUpdateUserData()
return useMutation({
mutationKey: ["signUp"],
mutationFn: async ({email, password, firstName, lastName}: { email: string, password: string, firstName: string, lastName: string }) => {
const res = await auth().createUserWithEmailAndPassword(email, password);
await updateUserData({newUserData: {userType: ProfileType.PARENT, firstName: firstName, lastName: lastName}, customUser: res.user});
}
});
}

View File

@ -0,0 +1,30 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation} from "react-query";
import firestore from "@react-native-firebase/firestore";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
import {FirebaseAuthTypes} from "@react-native-firebase/auth";
export const useUpdateUserData = () => {
const {user: currentUser, setProfileData} = useAuthContext()
return useMutation({
mutationKey: ["updateUserData"],
mutationFn: async ({newUserData, customUser}: {newUserData: Partial<UserProfile>, customUser?: FirebaseAuthTypes.User }) => {
const user = currentUser ?? customUser
if (user) {
try {
await firestore()
.collection("Profiles")
.doc(user.uid)
.update(newUserData);
const profileData = await firestore().collection("Profiles").doc(user?.uid!).get()
setProfileData(profileData.data() as UserProfile)
} catch (e) {
console.error(e)
}
}
}
})
}

File diff suppressed because it is too large Load Diff

View File

@ -12,12 +12,11 @@
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
5E2AAAAB828382553E0AFE4A /* Pods_cally.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6DBBD766136AC86F76B424D /* Pods_cally.framework */; };
5FBBDDA247284BE0B0D73DDC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F56C9EADA6FA4AEAA71245EB /* GoogleService-Info.plist */; };
A701043C80FD45C395532756 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C73694A22A420999475DA2 /* noop-file.swift */; };
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
D29BD67F528397E7E85920CE /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F20F68FCCB33056D70B2396B /* PrivacyInfo.xcprivacy */; };
F3E0D64F2C5A9A32001E9A28 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F3E0D64E2C5A9A32001E9A28 /* GoogleService-Info.plist */; };
5FBBDDA247284BE0B0D73DDC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F56C9EADA6FA4AEAA71245EB /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -36,9 +35,8 @@
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
F20F68FCCB33056D70B2396B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = cally/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
F3E0D64E2C5A9A32001E9A28 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
F56C9EADA6FA4AEAA71245EB /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "cally/GoogleService-Info.plist"; sourceTree = "<group>"; };
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-cally/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
F56C9EADA6FA4AEAA71245EB /* GoogleService-Info.plist */ = {isa = PBXFileReference; name = "GoogleService-Info.plist"; path = "cally/GoogleService-Info.plist"; sourceTree = "<group>"; fileEncoding = 4; lastKnownFileType = text.plist.xml; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -90,7 +88,6 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
F3E0D64E2C5A9A32001E9A28 /* GoogleService-Info.plist */,
13B07FAE1A68108700A75B9A /* cally */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */,
@ -210,7 +207,6 @@
files = (
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
F3E0D64F2C5A9A32001E9A28 /* GoogleService-Info.plist in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
D29BD67F528397E7E85920CE /* PrivacyInfo.xcprivacy in Resources */,
5FBBDDA247284BE0B0D73DDC /* GoogleService-Info.plist in Resources */,
@ -297,6 +293,7 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-cally/Pods-cally-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/BoringSSL-GRPC/openssl_grpc.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
@ -306,6 +303,8 @@
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCrashlytics/FirebaseCrashlytics_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseFirestore/FirebaseFirestore_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseFirestoreInternal/FirebaseFirestoreInternal_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInstallations/FirebaseInstallations_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle",
@ -313,12 +312,17 @@
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/abseil/xcprivacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/gRPC-C++/gRPCCertificates-Cpp.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/gRPC-C++/grpcpp.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/gRPC-Core/grpc.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/leveldb-library/leveldb_Privacy.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/openssl_grpc.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
@ -328,6 +332,8 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreExtension_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCrashlytics_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseFirestore_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseFirestoreInternal_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseInstallations_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle",
@ -335,9 +341,13 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/xcprivacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates-Cpp.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/grpcpp.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/grpc.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/leveldb_Privacy.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@ -419,7 +429,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = "cally";
PRODUCT_NAME = cally;
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -450,7 +460,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = "cally";
PRODUCT_NAME = cally;
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";

View File

@ -4,6 +4,24 @@
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
<string>0A2A.1</string>
<string>3B52.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
@ -14,16 +32,6 @@
<string>C56D.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>0A2A.1</string>
<string>3B52.1</string>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
@ -33,14 +41,6 @@
<string>85F4.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>

2930
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,11 +17,15 @@
"dependencies": {
"@expo/vector-icons": "^14.0.2",
"@react-native-community/blur": "^4.4.0",
"@react-native-community/datetimepicker": "^8.2.0",
"@react-native-firebase/app": "^20.3.0",
"@react-native-firebase/auth": "^20.3.0",
"@react-native-firebase/crashlytics": "^20.3.0",
"@react-native-firebase/firestore": "^20.4.0",
"@react-native-firebase/functions": "^20.4.0",
"@react-navigation/drawer": "^6.7.2",
"@react-navigation/native": "^6.0.2",
"date-fns": "^3.6.0",
"expo": "~51.0.24",
"expo-build-properties": "~0.12.4",
"expo-constants": "~16.0.2",
@ -33,15 +37,21 @@
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7",
"expo-web-browser": "~13.0.3",
"firebase-admin": "^12.3.1",
"firebase-functions": "^5.1.0",
"jotai": "^2.9.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.74.3",
"react-native-big-calendar": "^4.14.0",
"react-native-calendars": "^1.1306.0",
"react-native-gesture-handler": "~2.16.1",
"react-native-linear-gradient": "^2.8.3",
"react-native-onboarding-swiper": "^1.3.0",
"react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1",
"react-native-svg": "^15.6.0",
"react-native-ui-lib": "^7.27.0",
"react-native-web": "~0.19.10",
"react-query": "^3.39.3"
@ -50,6 +60,7 @@
"@babel/core": "^7.20.0",
"@types/jest": "^29.5.12",
"@types/react": "~18.2.45",
"@types/react-native-onboarding-swiper": "^1.1.9",
"@types/react-test-renderer": "^18.0.7",
"jest": "^29.2.1",
"jest-expo": "~51.0.3",

10492
yarn.lock Normal file

File diff suppressed because it is too large Load Diff