40
.github/workflows/ci-cd.yml
vendored
Executable 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
@ -20,3 +20,4 @@ expo-env.d.ts
|
|||||||
# @end expo-cli
|
# @end expo-cli
|
||||||
/ios/GoogleService-Info.plist
|
/ios/GoogleService-Info.plist
|
||||||
/ios/cally.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
|
/ios/cally.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
|
||||||
|
expo-env.d.ts
|
||||||
|
|||||||
15
.idea/git_toolbox_prj.xml
generated
Normal 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>
|
||||||
5
app.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "cally",
|
"name": "Cally - Planner",
|
||||||
"slug": "cally",
|
"slug": "cally",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
@ -15,7 +15,8 @@
|
|||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"bundleIdentifier": "com.cally.app",
|
"bundleIdentifier": "com.cally.app",
|
||||||
"googleServicesFile": "./ios/GoogleService-Info.plist"
|
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
||||||
|
"buildNumber": "5"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
|
|||||||
@ -1,12 +1,202 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { Drawer } from "expo-router/drawer";
|
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() {
|
export default function TabLayout() {
|
||||||
|
const { mutateAsync: signOut } = useSignOut();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
|
initialRouteName={"index"}
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: true,
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
import { View } from "react-native-ui-lib";
|
||||||
|
|
||||||
export default function Screen() {
|
export default function Screen() {
|
||||||
return (
|
return (
|
||||||
<View/>
|
<BrainDumpProvider>
|
||||||
)
|
<View>
|
||||||
|
<BrainDumpPage />
|
||||||
|
</View>
|
||||||
|
</BrainDumpProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -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() {
|
export default function Screen() {
|
||||||
return (
|
const [calendarHeight, setCalendarHeight] = useState(0);
|
||||||
<View/>
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -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() {
|
export default function Screen() {
|
||||||
return (
|
return (
|
||||||
<View/>
|
<GroceryProvider>
|
||||||
)
|
<GroceryWrapper />
|
||||||
|
</GroceryProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -1,70 +1,3 @@
|
|||||||
import { Image, StyleSheet, Platform } from 'react-native';
|
import Screen from "@/app/(auth)/calendar";
|
||||||
|
|
||||||
import { HelloWave } from '@/components/HelloWave';
|
export default Screen
|
||||||
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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import {Stack} from "expo-router";
|
|
||||||
|
|
||||||
export default function StackLayout () {
|
|
||||||
return <Stack screenOptions={{headerShown: false}}/>
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import {View} from "react-native-ui-lib";
|
|
||||||
|
|
||||||
export default function Screen() {
|
|
||||||
return (
|
|
||||||
<View/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -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() {
|
export default function Screen() {
|
||||||
return (
|
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%",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
5
app/(auth)/settings/_layout.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Stack } from "expo-router";
|
||||||
|
|
||||||
|
export default function StackLayout() {
|
||||||
|
return <Stack screenOptions={{ headerShown: false }} />;
|
||||||
|
}
|
||||||
15
app/(auth)/settings/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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() {
|
export default function Screen() {
|
||||||
return (
|
return (
|
||||||
<View/>
|
<ToDosContextProvider>
|
||||||
)
|
<ScrollView>
|
||||||
|
<View backgroundColor="#f9f8f7">
|
||||||
|
<HeaderTemplate message="Here are your To Do's" isWelcome={true} />
|
||||||
|
<ProgressCard />
|
||||||
|
<ToDosList />
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
<AddChore />
|
||||||
|
</ToDosContextProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import {Stack} from "expo-router";
|
||||||
|
|
||||||
|
export default function Layout() {
|
||||||
|
return <Stack screenOptions={{title: ""}}/>
|
||||||
|
}
|
||||||
7
app/(unauth)/index.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Entry from "@/components/pages/main/Entry";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
return (
|
||||||
|
<Entry/>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
app/(unauth)/onboarding_flow/index.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Entry from "@/components/pages/main/Entry";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
return <Entry />;
|
||||||
|
}
|
||||||
@ -1,15 +1,16 @@
|
|||||||
import { Link, Stack } from 'expo-router';
|
import {Link, Stack, usePathname} from 'expo-router';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
import { ThemedView } from '@/components/ThemedView';
|
import { ThemedView } from '@/components/ThemedView';
|
||||||
|
|
||||||
export default function NotFoundScreen() {
|
export default function NotFoundScreen() {
|
||||||
|
const route = usePathname()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||||
<ThemedView style={styles.container}>
|
<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}>
|
<Link href="/" style={styles.link}>
|
||||||
<ThemedText type="link">Go to home screen!</ThemedText>
|
<ThemedText type="link">Go to home screen!</ThemedText>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -6,10 +6,25 @@ import { useEffect } from 'react';
|
|||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
|
|
||||||
import {useColorScheme} from '@/hooks/useColorScheme';
|
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.
|
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||||
SplashScreen.preventAutoHideAsync();
|
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() {
|
export default function RootLayout() {
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const [loaded] = useFonts({
|
const [loaded] = useFonts({
|
||||||
@ -27,6 +42,8 @@ export default function RootLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<AuthContextProvider>
|
||||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen name="(auth)" options={{headerShown: false}}/>
|
<Stack.Screen name="(auth)" options={{headerShown: false}}/>
|
||||||
@ -34,5 +51,7 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="+not-found"/>
|
<Stack.Screen name="+not-found"/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</AuthContextProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
assets/images/splash-clock.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
6
assets/svgs/brain_dump-menu.svg
Normal 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 |
3
assets/svgs/calendar-menu.svg
Normal 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 |
6
assets/svgs/chores-menu.svg
Normal 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 |
6
assets/svgs/groceries-menu.svg
Normal 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 |
3
assets/svgs/reminders-menu.svg
Normal 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 |
3
assets/svgs/todos-menu.svg
Normal 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 |
54
components/pages/brain_dump/BrainDumpPage.tsx
Normal 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;
|
||||||
30
components/pages/brain_dump/DumpItem.tsx
Normal 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;
|
||||||
27
components/pages/brain_dump/DumpList.tsx
Normal 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;
|
||||||
124
components/pages/brain_dump/MoveBrainDump.tsx
Normal 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;
|
||||||
89
components/pages/calendar/AddEventDialog.tsx
Normal 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)}/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
260
components/pages/calendar/ManuallyAddEventModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
92
components/pages/grocery/AddGroceryItem.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
35
components/pages/grocery/CategoryDropdown.tsx
Normal 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;
|
||||||
98
components/pages/grocery/EditGroceryFrequency.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
28
components/pages/grocery/EditGroceryItem.tsx
Normal 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
|
||||||
161
components/pages/grocery/GroceryItem.tsx
Normal 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;
|
||||||
198
components/pages/grocery/GroceryList.tsx
Normal 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;
|
||||||
22
components/pages/grocery/GroceryWrapper.tsx
Normal 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;
|
||||||
60
components/pages/grocery/TopDisplay.tsx
Normal 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;
|
||||||
29
components/pages/main/Entry.tsx
Normal 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;
|
||||||
69
components/pages/main/SignInPage.tsx
Normal 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;
|
||||||
145
components/pages/main/SignUpPage.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
22
components/pages/main/WelcomeSplash.tsx
Normal 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;
|
||||||
84
components/pages/onboarding/OnboardingFlow.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
306
components/pages/reminders/addReminderModal.tsx
Normal 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;
|
||||||
129
components/pages/reminders/remindersList.tsx
Normal 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;
|
||||||
123
components/pages/settings/CalendarSettingsPage.tsx
Normal 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;
|
||||||
33
components/pages/settings/ChoreRewardSettings.tsx
Normal 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;
|
||||||
107
components/pages/settings/SettingsPage.tsx
Normal 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;
|
||||||
80
components/pages/settings/UserSettings.tsx
Normal 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;
|
||||||
12
components/pages/settings/user_settings_views/GroupsList.tsx
Normal 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;
|
||||||
43
components/pages/settings/user_settings_views/MyGroup.tsx
Normal 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;
|
||||||
102
components/pages/settings/user_settings_views/MyProfile.tsx
Normal 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;
|
||||||
274
components/pages/todos/AddChore.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
48
components/pages/todos/ProgressCard.tsx
Normal 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;
|
||||||
68
components/pages/todos/ToDoItem.tsx
Normal 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;
|
||||||
78
components/pages/todos/ToDosList.tsx
Normal 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;
|
||||||
51
components/shared/DrawerButton.tsx
Normal 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;
|
||||||
25
components/shared/HeaderTemplate.tsx
Normal 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;
|
||||||
43
components/shared/PointsSlider.tsx
Normal 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;
|
||||||
34
components/ui/ButtonOutlined.tsx
Normal 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
@ -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
@ -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: "What’s For Dinner",
|
||||||
|
description:
|
||||||
|
"What’s one meal you’d love to have for dinner this week? Let’s get creative with new ideas we haven’t tried yet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "The Best Thing About Today",
|
||||||
|
description:
|
||||||
|
"What was the highlight of your day? Let’s 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:
|
||||||
|
"What’s a favorite memory from your childhood? Let’s take a trip down memory lane and share some of the moments that made us smile.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: "A New Family Tradition",
|
||||||
|
description:
|
||||||
|
"What’s 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
@ -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)!;
|
||||||
67
contexts/RemindersContext.tsx
Normal 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)!;
|
||||||
53
contexts/SettingsContext.tsx
Normal 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
@ -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
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"default": "cally-family-calendar"
|
||||||
|
}
|
||||||
|
}
|
||||||
69
firebase/.gitignore
vendored
Normal 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
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
4
firebase/firestore.indexes.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"indexes": [],
|
||||||
|
"fieldOverrides": []
|
||||||
|
}
|
||||||
19
firebase/firestore.rules
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
firebase/functions/.eslintrc.js
Normal 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
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
*.local
|
||||||
45
firebase/functions/index.js
Normal 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
26
firebase/functions/package.json
Normal 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
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
13
hooks/firebase/types/eventData.ts
Normal 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[]
|
||||||
|
}
|
||||||
34
hooks/firebase/types/profileTypes.ts
Normal 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;
|
||||||
|
}
|
||||||
25
hooks/firebase/useCreateEvent.ts
Normal 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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
23
hooks/firebase/useCreateSubUser.ts
Normal 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"]})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
14
hooks/firebase/useGetCaregivers.ts
Normal 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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
34
hooks/firebase/useGetChildrenByParentId.ts
Normal 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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
31
hooks/firebase/useGetEvents.ts
Normal 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;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
18
hooks/firebase/useSignIn.ts
Normal 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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
11
hooks/firebase/useSignOut.ts
Normal 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()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
16
hooks/firebase/useSignUp.ts
Normal 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});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
30
hooks/firebase/useUpdateUserData.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
1196
ios/Podfile.lock
@ -12,12 +12,11 @@
|
|||||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||||
5E2AAAAB828382553E0AFE4A /* Pods_cally.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6DBBD766136AC86F76B424D /* Pods_cally.framework */; };
|
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 */; };
|
A701043C80FD45C395532756 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C73694A22A420999475DA2 /* noop-file.swift */; };
|
||||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||||
D29BD67F528397E7E85920CE /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F20F68FCCB33056D70B2396B /* PrivacyInfo.xcprivacy */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@ -36,9 +35,8 @@
|
|||||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -90,7 +88,6 @@
|
|||||||
83CBB9F61A601CBA00E9B192 = {
|
83CBB9F61A601CBA00E9B192 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F3E0D64E2C5A9A32001E9A28 /* GoogleService-Info.plist */,
|
|
||||||
13B07FAE1A68108700A75B9A /* cally */,
|
13B07FAE1A68108700A75B9A /* cally */,
|
||||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||||
83CBBA001A601CBA00E9B192 /* Products */,
|
83CBBA001A601CBA00E9B192 /* Products */,
|
||||||
@ -210,7 +207,6 @@
|
|||||||
files = (
|
files = (
|
||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||||
F3E0D64F2C5A9A32001E9A28 /* GoogleService-Info.plist in Resources */,
|
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||||
D29BD67F528397E7E85920CE /* PrivacyInfo.xcprivacy in Resources */,
|
D29BD67F528397E7E85920CE /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
5FBBDDA247284BE0B0D73DDC /* GoogleService-Info.plist in Resources */,
|
5FBBDDA247284BE0B0D73DDC /* GoogleService-Info.plist in Resources */,
|
||||||
@ -297,6 +293,7 @@
|
|||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-cally/Pods-cally-resources.sh",
|
"${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/EXConstants.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_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}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCrashlytics/FirebaseCrashlytics_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}/FirebaseInstallations/FirebaseInstallations_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_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}/PromisesObjC/FBLPromises_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.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-launcher/EXDevLauncher.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.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";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputPaths = (
|
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}/EXConstants.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_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}/FirebaseCoreExtension_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_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}/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}/FirebaseInstallations_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_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}/FBLPromises_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_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}/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}/EXDevLauncher.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.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;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
@ -419,7 +429,7 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
||||||
PRODUCT_NAME = "cally";
|
PRODUCT_NAME = cally;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -450,7 +460,7 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
||||||
PRODUCT_NAME = "cally";
|
PRODUCT_NAME = cally;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|||||||
@ -4,6 +4,24 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPITypes</key>
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
<array>
|
<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>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
@ -14,16 +32,6 @@
|
|||||||
<string>C56D.1</string>
|
<string>C56D.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</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>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||||
@ -33,14 +41,6 @@
|
|||||||
<string>85F4.1</string>
|
<string>85F4.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
|
||||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
||||||
<array>
|
|
||||||
<string>35F9.1</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
</array>
|
||||||
<key>NSPrivacyCollectedDataTypes</key>
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
<array/>
|
<array/>
|
||||||
|
|||||||
2930
package-lock.json
generated
11
package.json
@ -17,11 +17,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
"@react-native-community/blur": "^4.4.0",
|
"@react-native-community/blur": "^4.4.0",
|
||||||
|
"@react-native-community/datetimepicker": "^8.2.0",
|
||||||
"@react-native-firebase/app": "^20.3.0",
|
"@react-native-firebase/app": "^20.3.0",
|
||||||
"@react-native-firebase/auth": "^20.3.0",
|
"@react-native-firebase/auth": "^20.3.0",
|
||||||
"@react-native-firebase/crashlytics": "^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/drawer": "^6.7.2",
|
||||||
"@react-navigation/native": "^6.0.2",
|
"@react-navigation/native": "^6.0.2",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
"expo": "~51.0.24",
|
"expo": "~51.0.24",
|
||||||
"expo-build-properties": "~0.12.4",
|
"expo-build-properties": "~0.12.4",
|
||||||
"expo-constants": "~16.0.2",
|
"expo-constants": "~16.0.2",
|
||||||
@ -33,15 +37,21 @@
|
|||||||
"expo-status-bar": "~1.12.1",
|
"expo-status-bar": "~1.12.1",
|
||||||
"expo-system-ui": "~3.0.7",
|
"expo-system-ui": "~3.0.7",
|
||||||
"expo-web-browser": "~13.0.3",
|
"expo-web-browser": "~13.0.3",
|
||||||
|
"firebase-admin": "^12.3.1",
|
||||||
|
"firebase-functions": "^5.1.0",
|
||||||
"jotai": "^2.9.1",
|
"jotai": "^2.9.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.74.3",
|
"react-native": "0.74.3",
|
||||||
|
"react-native-big-calendar": "^4.14.0",
|
||||||
"react-native-calendars": "^1.1306.0",
|
"react-native-calendars": "^1.1306.0",
|
||||||
"react-native-gesture-handler": "~2.16.1",
|
"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-reanimated": "~3.10.1",
|
||||||
"react-native-safe-area-context": "4.10.5",
|
"react-native-safe-area-context": "4.10.5",
|
||||||
"react-native-screens": "3.31.1",
|
"react-native-screens": "3.31.1",
|
||||||
|
"react-native-svg": "^15.6.0",
|
||||||
"react-native-ui-lib": "^7.27.0",
|
"react-native-ui-lib": "^7.27.0",
|
||||||
"react-native-web": "~0.19.10",
|
"react-native-web": "~0.19.10",
|
||||||
"react-query": "^3.39.3"
|
"react-query": "^3.39.3"
|
||||||
@ -50,6 +60,7 @@
|
|||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/react": "~18.2.45",
|
"@types/react": "~18.2.45",
|
||||||
|
"@types/react-native-onboarding-swiper": "^1.1.9",
|
||||||
"@types/react-test-renderer": "^18.0.7",
|
"@types/react-test-renderer": "^18.0.7",
|
||||||
"jest": "^29.2.1",
|
"jest": "^29.2.1",
|
||||||
"jest-expo": "~51.0.3",
|
"jest-expo": "~51.0.3",
|
||||||
|
|||||||