mirror of
https://github.com/urosran/cally.git
synced 2025-07-09 22:57:16 +00:00
birthday through qr, deleteFam function
This commit is contained in:
117
app/(unauth)/birthday_page.tsx
Normal file
117
app/(unauth)/birthday_page.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { Button, Text, View, DateTimePicker } from "react-native-ui-lib";
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "expo-router";
|
||||
import { Platform, StyleSheet } from "react-native";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
|
||||
|
||||
export default function BirthdayScreen() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuthContext();
|
||||
const [date, setDate] = useState(new Date());
|
||||
const { mutateAsync: updateUserData } = useUpdateUserData();
|
||||
|
||||
const onDateChange = (event: any, selectedDate?: Date) => {
|
||||
const currentDate = selectedDate || date;
|
||||
setDate(currentDate);
|
||||
};
|
||||
|
||||
const handleContinue = async () => {
|
||||
try {
|
||||
updateUserData({
|
||||
newUserData: {
|
||||
birthday: date,
|
||||
},
|
||||
}).then(() => router.push("/(unauth)/cal_sync"));
|
||||
} catch (error) {
|
||||
console.error("Error saving birthday:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getMaxDate = () => {
|
||||
const date = new Date();
|
||||
date.setFullYear(date.getFullYear() - 3); // Minimum age: 3 years
|
||||
return date;
|
||||
};
|
||||
|
||||
const getMinDate = () => {
|
||||
const date = new Date();
|
||||
date.setFullYear(date.getFullYear() - 18); // Maximum age: 18 years
|
||||
return date;
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 21,
|
||||
paddingBottom: 45,
|
||||
paddingTop: "20%",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<View gap-13 width={"100%"} marginB-20>
|
||||
<Text style={{ fontSize: 40, fontFamily: "Manrope_600SemiBold" }}>
|
||||
When's your birthday?
|
||||
</Text>
|
||||
<Text color={"#919191"} style={{ fontSize: 20 }}>
|
||||
We'll use this to celebrate your special day!
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View width={"100%"} flexG>
|
||||
<DateTimePicker
|
||||
value={date}
|
||||
mode="date"
|
||||
minimumDate={getMinDate()}
|
||||
maximumDate={getMaxDate()}
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
const validDate = new Date(date);
|
||||
if (!isNaN(validDate.getTime())) {
|
||||
setDate(validDate);
|
||||
}
|
||||
}
|
||||
}}
|
||||
style={styles.textfield}
|
||||
textAlign="center"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View flexG />
|
||||
|
||||
<View width={"100%"}>
|
||||
<Button
|
||||
label="Continue"
|
||||
onPress={handleContinue}
|
||||
style={{
|
||||
height: 50,
|
||||
}}
|
||||
backgroundColor="#fd1775"
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||
fontSize: 16,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
textfield: {
|
||||
backgroundColor: "white",
|
||||
marginVertical: 100,
|
||||
padding: 30,
|
||||
height: 44,
|
||||
borderRadius: 50,
|
||||
fontFamily: "PlusJakartaSans_300Light",
|
||||
fontSize: 15,
|
||||
color: "#919191",
|
||||
alignContent: "center",
|
||||
},
|
||||
});
|
@ -49,7 +49,7 @@ export default function Screen() {
|
||||
|
||||
const debouncedRouterReplace = useCallback(
|
||||
debounce(() => {
|
||||
router.push("/(unauth)/cal_sync");
|
||||
router.push("/(unauth)/birthday_page");
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
@ -26,11 +26,13 @@ import { useDeleteUser } from "@/hooks/firebase/useDeleteUser";
|
||||
import { useUpdateHouseholdName } from "@/hooks/firebase/useUpdateHouseholdName";
|
||||
import { useGetHouseholdName } from "@/hooks/firebase/useGetHouseholdName";
|
||||
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
||||
import { useDeleteFamily } from "@/hooks/firebase/useDeleteFamily";
|
||||
|
||||
const MyProfile = () => {
|
||||
const { user, profileData } = useAuthContext();
|
||||
const { data: familyMembers } = useGetFamilyMembers();
|
||||
const [takenColors, setTakenColors] = useState<string[]>([]);
|
||||
const { mutate: deleteFamily, isLoading } = useDeleteFamily();
|
||||
|
||||
const { data: hhName, refetch: refetchHHName } = useGetHouseholdName(
|
||||
profileData.familyId
|
||||
@ -181,6 +183,19 @@ const MyProfile = () => {
|
||||
debouncedUpdateUserData(color);
|
||||
};
|
||||
|
||||
const handleDeleteFamily = () => {
|
||||
if(profileData?.familyId){
|
||||
deleteFamily({familyId: profileData?.familyId}, {
|
||||
onSuccess: () => {
|
||||
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log("from delete fam:\n" + error);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedUpdateUserData = useCallback(
|
||||
debounce(async (color: string) => {
|
||||
try {
|
||||
@ -568,7 +583,8 @@ const MyProfile = () => {
|
||||
onDismiss={handleHideDeleteDialog}
|
||||
onConfirm={() => {
|
||||
if(isDeleteFamily)
|
||||
console.log('put delete family func here');
|
||||
//deletes family
|
||||
handleDeleteFamily();
|
||||
else
|
||||
{
|
||||
//deletes profile
|
||||
|
@ -1771,3 +1771,83 @@ exports.updateHouseholdTimestampOnEventUpdate = functions.firestore
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
exports.deleteFamily = functions.https.onCall(async (data, context) => {
|
||||
if (!context.auth) {
|
||||
throw new functions.https.HttpsError('unauthenticated', 'User must be authenticated');
|
||||
}
|
||||
|
||||
const { familyId } = data;
|
||||
|
||||
if (!familyId) {
|
||||
throw new functions.https.HttpsError('invalid-argument', 'Family ID is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const db = admin.firestore();
|
||||
|
||||
const requestingUserProfile = await db.collection('Profiles')
|
||||
.doc(context.auth.uid)
|
||||
.get();
|
||||
|
||||
if (!requestingUserProfile.exists || requestingUserProfile.data().userType !== 'parent') {
|
||||
throw new functions.https.HttpsError('permission-denied', 'Only parents can delete families');
|
||||
}
|
||||
|
||||
if (requestingUserProfile.data().familyId !== familyId) {
|
||||
throw new functions.https.HttpsError('permission-denied', 'You can not delete other families');
|
||||
}
|
||||
|
||||
const profilesSnapshot = await db.collection('Profiles')
|
||||
.where('familyId', '==', familyId)
|
||||
.get();
|
||||
|
||||
const batch = db.batch();
|
||||
const profileIds = [];
|
||||
|
||||
for (const profile of profilesSnapshot.docs) {
|
||||
const userId = profile.id;
|
||||
profileIds.push(userId);
|
||||
|
||||
const collections = [
|
||||
'BrainDumps',
|
||||
'Groceries',
|
||||
'Todos',
|
||||
'Events'
|
||||
];
|
||||
|
||||
for (const collectionName of collections) {
|
||||
const userDocsSnapshot = await db.collection(collectionName)
|
||||
.where('creatorId', '==', userId)
|
||||
.get();
|
||||
|
||||
userDocsSnapshot.docs.forEach(doc => {
|
||||
batch.delete(doc.ref);
|
||||
});
|
||||
}
|
||||
|
||||
batch.delete(profile.ref);
|
||||
}
|
||||
|
||||
const householdDoc = await db.collection('Households')
|
||||
.doc(familyId)
|
||||
.get();
|
||||
|
||||
if (householdDoc.exists) {
|
||||
batch.delete(householdDoc.ref);
|
||||
}
|
||||
|
||||
await batch.commit();
|
||||
|
||||
// Delete Firebase Auth accounts
|
||||
await Promise.all(profileIds.map(userId =>
|
||||
admin.auth().deleteUser(userId)
|
||||
));
|
||||
|
||||
return { success: true, message: 'Family deleted successfully' };
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting family:', error);
|
||||
throw new functions.https.HttpsError('internal', 'Error deleting family data');
|
||||
}
|
||||
});
|
32
hooks/firebase/useDeleteFamily.ts
Normal file
32
hooks/firebase/useDeleteFamily.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import functions from '@react-native-firebase/functions';
|
||||
import { Alert } from 'react-native';
|
||||
|
||||
export const useDeleteFamily = () => {
|
||||
const { user } = useAuthContext();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ["deleteFamily"],
|
||||
mutationFn: async ({ familyId }: { familyId: string }) => {
|
||||
if (!user) {
|
||||
throw new Error('User must be logged in');
|
||||
}
|
||||
|
||||
try {
|
||||
const deleteFamilyFunction = functions().httpsCallable('deleteFamily');
|
||||
const result = await deleteFamilyFunction({ familyId });
|
||||
return result.data;
|
||||
} catch (error: any) {
|
||||
if (error.code === 'permission-denied') {
|
||||
Alert.alert('Error', 'Only parents can delete families');
|
||||
} else if (error.code === 'unauthenticated') {
|
||||
Alert.alert('Error', 'Please log in to perform this action');
|
||||
} else {
|
||||
Alert.alert('Error', 'Failed to delete family. Please try again.');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
@ -1,194 +0,0 @@
|
||||
diff --git a/node_modules/react-native-big-calendar/build/index.js b/node_modules/react-native-big-calendar/build/index.js
|
||||
index 848ceba..f326b8e 100644
|
||||
--- a/node_modules/react-native-big-calendar/build/index.js
|
||||
+++ b/node_modules/react-native-big-calendar/build/index.js
|
||||
@@ -9,6 +9,17 @@ var isoWeek = require('dayjs/plugin/isoWeek');
|
||||
var React = require('react');
|
||||
var reactNative = require('react-native');
|
||||
var calendarize = require('calendarize');
|
||||
+var {
|
||||
+ startOfDay,
|
||||
+ endOfDay,
|
||||
+ startOfWeek,
|
||||
+ isAfter,
|
||||
+ isBefore,
|
||||
+ isSameDay,
|
||||
+ differenceInDays,
|
||||
+ add,
|
||||
+ getTime
|
||||
+} = require('date-fns');
|
||||
|
||||
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
||||
|
||||
@@ -1000,87 +1011,91 @@ function _CalendarBodyForMonthView(_a) {
|
||||
var start = _a.start, end = _a.end;
|
||||
return day.isBetween(dayjs__default["default"](start).startOf('day'), dayjs__default["default"](end).endOf('day'), null, '[)');
|
||||
});
|
||||
- }
|
||||
- else {
|
||||
- /**
|
||||
- * Better way to sort overlapping events that spans accross multiple days
|
||||
- * For example, if you want following events
|
||||
- * Event 1, start = 01/01 12:00, end = 02/01 12:00
|
||||
- * Event 2, start = 02/01 12:00, end = 03/01 12:00
|
||||
- * Event 3, start = 03/01 12:00, end = 04/01 12:00
|
||||
- *
|
||||
- * When drawing calendar in month view, event 3 should be placed at 3rd index for 03/01, because Event 2 are placed at 2nd index for 02/01 and 03/01
|
||||
- *
|
||||
- */
|
||||
- var min_1 = day.startOf('day'), max_1 = day.endOf('day');
|
||||
- //filter all events that starts from the current week until the current day, and sort them by reverse starting time
|
||||
- var filteredEvents_1 = events
|
||||
- .filter(function (_a) {
|
||||
- var start = _a.start, end = _a.end;
|
||||
- return dayjs__default["default"](end).isAfter(day.startOf('week')) && dayjs__default["default"](start).isBefore(max_1);
|
||||
- })
|
||||
- .sort(function (a, b) {
|
||||
- if (dayjs__default["default"](a.start).isSame(b.start, 'day')) {
|
||||
- var aDuration = dayjs__default["default"].duration(dayjs__default["default"](a.end).diff(dayjs__default["default"](a.start))).days();
|
||||
- var bDuration = dayjs__default["default"].duration(dayjs__default["default"](b.end).diff(dayjs__default["default"](b.start))).days();
|
||||
- return aDuration - bDuration;
|
||||
- }
|
||||
- return b.start.getTime() - a.start.getTime();
|
||||
+ } else {
|
||||
+ // Convert day once and cache all the commonly used dates/times
|
||||
+ const jsDay = day?.toDate?.() || day;
|
||||
+ const weekStart = startOfWeek(jsDay);
|
||||
+ var min_1 = startOfDay(jsDay);
|
||||
+ var max_1 = endOfDay(jsDay);
|
||||
+ const max1Time = getTime(max_1);
|
||||
+ const min1Time = getTime(min_1);
|
||||
+
|
||||
+ // Pre-process events with dates and cache timestamps
|
||||
+ const processedEvents = events.map(event => {
|
||||
+ const startDay = event.start?.toDate?.() || new Date(event.start);
|
||||
+ const endDay = event.end?.toDate?.() || new Date(event.end);
|
||||
+ return {
|
||||
+ ...event,
|
||||
+ startDay,
|
||||
+ endDay,
|
||||
+ startTime: getTime(startDay),
|
||||
+ endTime: getTime(endDay),
|
||||
+ startDayStart: startOfDay(startDay)
|
||||
+ };
|
||||
});
|
||||
- /**
|
||||
- * find the most relevant min date to filter the events
|
||||
- * in the example:
|
||||
- * 1. when rendering for 01/01, min date will be 01/01 (start of day for event 1)
|
||||
- * 2. when rendering for 02/01, min date will be 01/01 (start of day for event 1)
|
||||
- * 3. when rendering for 03/01, min date will be 01/01 (start of day for event 1)
|
||||
- * 4. when rendering for 04/01, min date will be 01/01 (start of day for event 1)
|
||||
- * 5. when rendering for 05/01, min date will be 05/01 (no event overlaps with 05/01)
|
||||
- */
|
||||
- filteredEvents_1.forEach(function (_a) {
|
||||
- var start = _a.start, end = _a.end;
|
||||
- if (dayjs__default["default"](end).isAfter(min_1) && dayjs__default["default"](start).isBefore(min_1)) {
|
||||
- min_1 = dayjs__default["default"](start).startOf('day');
|
||||
+
|
||||
+ // Filter events within the weekly range and sort by reverse start time
|
||||
+ let filteredEvents_1 = processedEvents
|
||||
+ .filter(({ startTime, endTime }) =>
|
||||
+ endTime > getTime(weekStart) && startTime < max1Time
|
||||
+ )
|
||||
+ .sort((a, b) => {
|
||||
+ if (isSameDay(a.startDay, b.startDay)) {
|
||||
+ // Pre-calculate durations since they're used in sorting
|
||||
+ const aDuration = differenceInDays(a.endDay, a.startDay);
|
||||
+ const bDuration = differenceInDays(b.endDay, b.startDay);
|
||||
+ return bDuration - aDuration;
|
||||
+ }
|
||||
+ return b.startTime - a.startTime;
|
||||
+ });
|
||||
+
|
||||
+ // Update min_1 to the earliest startDay for overlapping events
|
||||
+ for (const event of filteredEvents_1) {
|
||||
+ if (event.endTime > min1Time && event.startTime < min1Time) {
|
||||
+ min_1 = event.startDayStart;
|
||||
+ break; // We only need the first one due to the sort order
|
||||
}
|
||||
- });
|
||||
+ }
|
||||
+
|
||||
+ // Filter to keep only events that overlap the min to max range, then reverse
|
||||
+ const min1TimeUpdated = getTime(min_1);
|
||||
filteredEvents_1 = filteredEvents_1
|
||||
- .filter(function (_a) {
|
||||
- var start = _a.start, end = _a.end;
|
||||
- return dayjs__default["default"](end).endOf('day').isAfter(min_1) && dayjs__default["default"](start).isBefore(max_1);
|
||||
- })
|
||||
+ .filter(({ startTime, endDay }) =>
|
||||
+ getTime(endOfDay(endDay)) > min1TimeUpdated && startTime < max1Time
|
||||
+ )
|
||||
.reverse();
|
||||
- /**
|
||||
- * We move eligible event to the top
|
||||
- * For example, when rendering for 03/01, Event 3 should be moved to the top, since there is a gap left by Event 1
|
||||
- */
|
||||
- var finalEvents_1 = [];
|
||||
- var tmpDay_1 = day.startOf('week');
|
||||
- //re-sort events from the start of week until the calendar cell date
|
||||
- //optimize sorting of event nodes and make sure that no empty gaps are left on top of calendar cell
|
||||
- while (!tmpDay_1.isAfter(day)) {
|
||||
- filteredEvents_1.forEach(function (event) {
|
||||
- if (dayjs__default["default"](event.end).isBefore(tmpDay_1.startOf('day'))) {
|
||||
- var eventToMoveUp = filteredEvents_1.find(function (e) {
|
||||
- return dayjs__default["default"](e.start).startOf('day').isSame(tmpDay_1.startOf('day'));
|
||||
- });
|
||||
- if (eventToMoveUp != undefined) {
|
||||
- //remove eventToMoveUp from finalEvents first
|
||||
- if (finalEvents_1.indexOf(eventToMoveUp) > -1) {
|
||||
- finalEvents_1.splice(finalEvents_1.indexOf(eventToMoveUp), 1);
|
||||
- }
|
||||
- if (finalEvents_1.indexOf(event) > -1) {
|
||||
- finalEvents_1.splice(finalEvents_1.indexOf(event), 1, eventToMoveUp);
|
||||
- }
|
||||
- else {
|
||||
+
|
||||
+ // Move eligible events to the top, preventing duplicate entries
|
||||
+ const finalEvents_1 = [];
|
||||
+ const seenEvents = new Set(); // Use Set for faster lookups
|
||||
+ let tmpDay_1 = weekStart;
|
||||
+
|
||||
+ while (!isAfter(tmpDay_1, jsDay)) {
|
||||
+ const tmpDayTime = getTime(tmpDay_1);
|
||||
+
|
||||
+ for (const event of filteredEvents_1) {
|
||||
+ const eventEndDayTime = getTime(startOfDay(event.endDay));
|
||||
+
|
||||
+ if (!seenEvents.has(event)) {
|
||||
+ if (eventEndDayTime < tmpDayTime) {
|
||||
+ // Find event starting on tmpDay
|
||||
+ const eventToMoveUp = filteredEvents_1.find(e =>
|
||||
+ isSameDay(e.startDayStart, tmpDay_1)
|
||||
+ );
|
||||
+ if (eventToMoveUp && !seenEvents.has(eventToMoveUp)) {
|
||||
finalEvents_1.push(eventToMoveUp);
|
||||
+ seenEvents.add(eventToMoveUp);
|
||||
}
|
||||
+ } else {
|
||||
+ finalEvents_1.push(event);
|
||||
+ seenEvents.add(event);
|
||||
}
|
||||
}
|
||||
- else if (finalEvents_1.indexOf(event) == -1) {
|
||||
- finalEvents_1.push(event);
|
||||
- }
|
||||
- });
|
||||
- tmpDay_1 = tmpDay_1.add(1, 'day');
|
||||
+ }
|
||||
+ tmpDay_1 = add(tmpDay_1, { days: 1 });
|
||||
}
|
||||
+
|
||||
return finalEvents_1;
|
||||
}
|
||||
}, [events, sortedMonthView]);
|
||||
@@ -1311,7 +1326,7 @@ function _CalendarHeader(_a) {
|
||||
!stringHasContent(dayHeaderHighlightColor) &&
|
||||
u['mt-6'],
|
||||
] }, date.format('D')))),
|
||||
- showAllDayEventCell ? (React__namespace.createElement(reactNative.View, { style: [
|
||||
+ showAllDayEventCell ? (React__namespace.createElement(reactNative.ScrollView, { style: [
|
||||
u['border-l'],
|
||||
{ borderColor: theme.palette.gray['200'] },
|
||||
{ height: cellHeight },
|
Reference in New Issue
Block a user