211 Commits

Author SHA1 Message Date
7a7b1902a8 adujstments 2025-02-16 01:09:46 +01:00
c184eb3293 adujstments 2025-02-16 01:07:12 +01:00
d9ee1cd921 Merge branch 'dev' 2025-02-15 21:08:30 +01:00
1b5024089d Merge remote-tracking branch 'origin/dev' into dev 2025-02-15 18:40:00 +01:00
59664488e8 Reimplementation of the todo take turns feature to fix issues with only one assignee showing 2025-02-15 18:39:54 +01:00
bb1fb8d8f7 default Household name 2025-02-15 17:40:42 +01:00
b804f37037 Merge branch 'dev'
# Conflicts:
#	app/_layout.tsx
#	firebase/functions/index.js
2025-02-15 16:44:45 +01:00
f649828d80 fixes, deleteFamily function, household signup 2025-02-15 00:34:42 +01:00
718fd562ff cicd adjustment 2025-02-14 19:18:01 +01:00
a855e6d3bf cicd adjustment 2025-02-14 19:16:51 +01:00
e04441bd81 Calendar controls fix 2025-02-14 15:05:42 +01:00
f9a5e76162 Calendar controls fix 2025-02-12 01:11:28 +01:00
479e7c5f56 Merge branch 'dev'
# Conflicts:
#	firebase/functions/index.js
2025-02-12 00:43:48 +01:00
a8957c7ac7 birthday through qr, deleteFam function 2025-02-12 00:05:39 +01:00
3d15d7bb74 Functions update 2025-02-07 14:12:23 +01:00
6ada9470c3 delete family popups and birthday 2025-02-06 22:47:18 +01:00
300ce82a4d Functions update 2025-02-06 09:44:06 +01:00
5ac4526baf Functions update 2025-02-06 01:03:34 +01:00
5cfec1544a Calendar improvements 2025-02-02 22:28:40 +01:00
e96210986e added birthday input 2025-01-28 21:12:08 +01:00
0998dc29d0 Month fixes 2025-01-26 17:15:13 +01:00
dc7d59eecc Merge branch 'refs/heads/dev' 2025-01-26 16:07:21 +01:00
14be80c6f0 Fixes 2025-01-24 01:12:18 +01:00
580104d052 Month cal changes 2025-01-23 02:16:07 +01:00
ba3ea5a6d2 fixed impossible event dates input 2025-01-22 21:41:24 +01:00
231e99ff8f Merge branch 'dev' 2025-01-19 15:28:00 +01:00
5333b8cbb7 - Reverted the stale time 2025-01-18 00:33:10 +01:00
dcca652019 - Improved fetching of events for family device
- Removed fetching events and considering if they're private or not
2025-01-18 00:31:48 +01:00
413164128b - Fixed events fetching in family view because there are events that don't have the private field
- Implemented marking events as private
2025-01-17 02:00:36 +01:00
bc77c403f2 Month calendar fix 2025-01-17 00:39:20 +01:00
1d4903b609 Merge branch 'dev'
# Conflicts:
#	components/pages/calendar/MonthCalendar.tsx
2025-01-17 00:26:36 +01:00
9c3b9b3663 fixed issue with pfp in tablet view 2025-01-16 15:50:19 +01:00
60953c34bc Month calendar 2025-01-16 00:22:09 +01:00
adafaf1dfe Merge branch 'dev' 2025-01-15 17:39:07 +01:00
0c1bed62d9 Merge remote-tracking branch 'origin/dev' into dev 2025-01-15 00:05:17 +01:00
5b7f54403c - Fixed events not fetching for the family device 2025-01-15 00:05:08 +01:00
81419c0a23 minor fixes 2025-01-14 20:54:56 +01:00
74c0369acf added reset password btn 2025-01-03 17:26:53 +01:00
0f07e46a82 Merge branch 'dev' 2024-12-30 21:01:59 +01:00
a4e27f9dc8 - Changed the upper limit for the todo points charts 2024-12-30 20:59:40 +01:00
6cfb33e227 - Todos point logic changes to use the current day for points 2024-12-30 20:54:33 +01:00
2a6357983f - Moved the touching area on the whole grocery card not just on the text 2024-12-30 20:49:16 +01:00
04d865cce9 - Fixed issue with family device not being able to see todos of the family
- Fixed issue with todos not being able to update on family device
2024-12-30 20:08:06 +01:00
f7dd31cae9 - Changed chore terms in the reports pages to todo 2024-12-30 18:39:21 +01:00
141337d2ce - Implemented spending of the saved points 2024-12-30 13:20:58 +01:00
ffe8cc72a1 - Sorted the user progress chart by day 2024-12-30 12:54:04 +01:00
239a08d4aa - Todos point logic and User progress UI 2024-12-30 12:09:12 +01:00
49be903553 Patch fix 2024-12-30 11:39:48 +03:00
8b6171a819 Merge remote-tracking branch 'origin/main' 2024-12-30 11:37:19 +03:00
7b809d826c - Todos point logic and Family progress UI 2024-12-29 23:01:24 +01:00
afa1046d02 - Fixed incorrect saving of the points per day 2024-12-29 21:39:49 +01:00
ff81570e15 - Rewrote add, edit groceries
- Added validation
2024-12-29 21:10:10 +01:00
81d791dd17 - Fixed merge conflicts 2024-12-29 19:06:38 +01:00
42b18856e4 Merge branch 'main' into dev
# Conflicts:
#	components/pages/calendar/DetailedCalendar.tsx
2024-12-29 18:59:46 +01:00
ab7a844bb5 fixed tablet calendar 2024-12-28 15:53:36 +01:00
28110b88cc Merge branch 'dev' 2024-12-28 00:57:25 +01:00
f1869f02f2 - Filtered out todos of deleted users when the Everyone option is selected in the filter 2024-12-25 22:49:07 +01:00
c14910447e - Implemented saving of points per week per day for user
- Implemented saving of the number of completed todos per user
- Changed "To do's" labels to "To Do"
2024-12-25 22:41:53 +01:00
7d3e39b77d Deletion fix 2024-12-24 23:19:23 +01:00
406f541163 ui changes 2024-12-24 20:44:30 +01:00
609d01b81c Merge branch 'dev' 2024-12-24 16:12:36 +01:00
10f6616cd0 Deletion fix 2024-12-24 16:07:18 +01:00
5c6915c23d ui changes, fixes 2024-12-24 13:03:42 +01:00
fb9be26e04 Merge remote-tracking branch 'origin/dev' into dev 2024-12-22 18:31:35 +01:00
2c6474007c - Implemented backend logic for saving weekly and all time points for todos 2024-12-22 18:31:22 +01:00
4866dd1b3f - Improved the todo update 2024-12-22 16:35:31 +01:00
c93d66d13d Phone header fix 2024-12-20 16:38:08 +01:00
1c9f9f865c Phone header fix 2024-12-20 13:43:20 +01:00
00092ae5f8 Merge branch 'dev' 2024-12-20 13:28:06 +01:00
6972066f91 menu button rework 2024-12-18 21:22:38 +01:00
63699d53f4 Revert "Don't spin on scroll"
This reverts commit 1903e4072c.
2024-12-18 17:37:29 +01:00
e86b2a5950 Don't spin on scroll 2024-12-18 17:37:06 +01:00
1903e4072c Don't spin on scroll 2024-12-18 17:37:00 +01:00
8aca8e46db Deleting event fix 2024-12-18 17:28:47 +01:00
b7fd8daddf - Added loading indicators when on groceries and todos, fixed margin issues in the todo page 2024-12-17 23:33:59 +01:00
8b802f492d - Changed the menu grocery label 2024-12-17 23:22:07 +01:00
3d96393972 - Added refetching if the family members when opening My group tab 2024-12-17 22:08:25 +01:00
956628bcb6 - Fixed issue with uploading images for other users 2024-12-17 21:52:55 +01:00
0b56b169a5 Merge remote-tracking branch 'origin/dev' into dev 2024-12-17 20:15:15 +01:00
4b67033aa8 - Implemented optimistic update when completing todos and groceries to avoid delays 2024-12-17 20:13:51 +01:00
3324a2fd6f reorganized calendar page, fixes 2024-12-17 18:12:47 +01:00
79ac1efe63 - Fixed issue with improper creation of Shopping todo 2024-12-17 12:40:11 +01:00
32a718953b - Added todo completed animation when child completes a todo 2024-12-17 11:31:50 +01:00
3564edadf2 - Added null check on household name update 2024-12-17 09:55:03 +01:00
f7860a5af8 - Fixed issue with parents not being able to approve groceries 2024-12-16 23:39:32 +01:00
8c28f049f7 - Changed number of weeks when creating recurring todo 2024-12-16 23:20:10 +01:00
003f3717e5 build fix 2024-12-16 20:19:44 +01:00
0733c25892 Merge remote-tracking branch 'origin/dev' into dev 2024-12-15 22:44:43 +01:00
335905335e -Fixed create todo issues 2024-12-15 22:44:33 +01:00
a4bd7920e4 Merge branch 'dev' 2024-12-15 22:24:06 +01:00
5be4e15885 Merge branch 'dev' of https://github.com/urosran/cally into dev 2024-12-15 18:53:45 +01:00
72aa06ad22 Notifications sorting, modal fixes 2024-12-15 18:45:04 +01:00
56f37cef94 Merge branch 'dev' 2024-12-15 18:25:03 +01:00
035f8af84b changed color options, removed cal_sync for fam_device 2024-12-15 18:11:45 +01:00
bb68a89cfb Fix stuff 2024-12-15 17:57:30 +01:00
818107efe3 Fix stuff 2024-12-15 17:54:01 +01:00
b59a8bf97d Merge remote-tracking branch 'origin/dev' into dev 2024-12-15 17:05:30 +01:00
5cafd10f09 - Changed the members dependency check when defining the todo filter options 2024-12-15 17:05:18 +01:00
c411990312 Fixes 2024-12-15 16:46:26 +01:00
70db8bdc0b New calendar 2024-12-15 16:29:34 +01:00
a6009beb03 Merge branch 'refs/heads/dev' 2024-12-15 16:28:07 +01:00
0aec9c1ee8 Merge branch 'dev' of https://github.com/urosran/cally into dev 2024-12-15 16:00:26 +01:00
4f96bb071b - Added validation on todo creation to avoid creating todos with Every week repeat type, but no selected days
- Reworked and improved the creation of repeat todo
2024-12-15 15:31:55 +01:00
67c896b9c6 Merge branch 'refs/heads/dev'
# Conflicts:
#	components/pages/calendar/ManuallyAddEventModal.tsx
2024-12-15 14:34:49 +01:00
d30ed9b130 - Fixed issue with todo filter not being updated after adding deleting users 2024-12-15 11:51:23 +01:00
5bb250f7ea fixed tablet pages 2024-12-11 21:20:27 +01:00
a305aedeeb settings for other users, fixed event time 2024-12-10 22:06:06 +01:00
fe9ee5334d Fixes 2024-12-10 22:00:36 +01:00
35608e350f Fixes 2024-12-10 20:05:15 +01:00
233427bf38 Merge branch 'dev' 2024-12-10 15:56:44 +01:00
2740907d63 fixed calendar not showing events on phone 2024-12-09 19:15:29 +01:00
ee04ba7850 IOS updates 2024-12-08 19:09:45 +01:00
ffb0df6bac IOS updates 2024-12-08 18:07:28 +01:00
3fe46f1954 tablet event and todo filtering 2024-12-08 16:49:39 +01:00
f382d403cd - Removed background color of the grocery items text for number of items 2024-12-08 14:06:19 +01:00
1c2f18e288 - Grocery list changes
- Removed the Add item button from the bottom and added an empty grocery box
2024-12-08 14:01:35 +01:00
47a0ea2f36 - Added Done category in the grocery list and pushed it to the bottom 2024-12-08 13:38:27 +01:00
12f1a51292 - Added delete button on the checked grocery items 2024-12-08 13:15:06 +01:00
ecee34bf26 - Added functionality to the add todo button in the groceries page to open a todo dialog with preselected title 2024-12-08 13:05:09 +01:00
33ada3505b bugfixes 2024-12-08 12:22:24 +01:00
89bb924b40 - Fixed issue with date resetting every time a repeat type is selected
- Showing take turns button when todo has repeat rule and has more than 1 assignee
2024-12-08 12:00:42 +01:00
94784b3706 Merge branch 'refs/heads/dev' 2024-12-08 11:38:59 +01:00
11115d151d - Filtered out the Family device user type from the todo filter options 2024-12-08 11:17:33 +01:00
9700a6b269 - Default todo repeat type - None 2024-12-07 11:39:27 +01:00
5866004f6a - Default selected attendee when creating events to be the creator of the event
- Added validation to not allow events without any attendees
2024-12-07 11:36:29 +01:00
a17e0f1e22 todo filtering, nanny shopping 2024-12-06 23:31:27 +01:00
8526d79ba1 refresh button, calendar margins 2024-12-05 11:32:54 +01:00
3880b0e1f4 bugfixes 2024-12-04 21:17:44 +01:00
771dba9658 bugfixes 2024-12-04 21:17:23 +01:00
1b288d095f family chart, other fixes 2024-12-04 19:48:04 +01:00
c86a355b97 notes, tablet sort, grocery list and other fixes 2024-12-02 18:46:36 +01:00
12b4ce3a70 fixes and bug features 2024-11-30 19:05:36 +01:00
92e879c3fc Merge remote-tracking branch 'origin/main' into dev 2024-11-30 18:02:28 +01:00
dfe7301f6d bugfixes 2024-11-28 08:38:52 +01:00
01b4fc2e33 tablet view fixes, grocery item fix 2024-11-27 20:17:48 +01:00
3dff040cea Revert calendar 2024-11-27 09:49:41 +01:00
95d5e74703 Calendar, syncing rework 2024-11-27 01:37:58 +01:00
f2af60111b Syncing rework 2024-11-26 21:13:54 +01:00
5cfdc84055 Merge branch 'dev'
# Conflicts:
#	app/(auth)/calendar/index.tsx
2024-11-26 21:01:10 +01:00
4b5900c652 added new colors, ui changes 2024-11-26 19:32:40 +01:00
ee749e1077 Notification batch updates and notifications page update 2024-11-23 16:39:56 +01:00
cd178b8a9d Merge branch 'dev'
# Conflicts:
#	components/pages/calendar/EventCalendar.tsx
2024-11-23 14:31:58 +01:00
d091aaa0ee Batch updates 2024-11-22 09:21:12 +01:00
06a3a2dc8f Notification changes 2024-11-22 03:25:16 +01:00
29ec985ffa - Fixed event coloring issue on the my view/family view 2024-11-21 23:53:18 +01:00
fb27079f10 - Added validation for title and attendees when creation/updating event 2024-11-21 23:32:50 +01:00
726668921b - Changed the location field style
- Added creator information on the events in the events dialog
2024-11-21 23:25:52 +01:00
c528f72b2e - Added created by label in the todo item 2024-11-21 22:55:54 +01:00
f74a6390a2 Merge branch 'dev' 2024-11-21 19:22:28 +01:00
edea14b32c Merge remote-tracking branch 'origin/dev' into dev 2024-11-20 23:58:21 +01:00
4f80525f13 - Added User update dialog, useUpdateSubUser.ts hook and implemented update of a selected user from the family group 2024-11-20 23:58:13 +01:00
830c1bf8ea Merge branch 'dev' of https://github.com/urosran/cally into dev 2024-11-20 21:09:53 +01:00
b5aac0af2a ui fixes and tweaks 2024-11-20 21:09:44 +01:00
b1a5d4c171 - Added validation when adding todos to restrict having todos without any assignees 2024-11-20 20:36:09 +01:00
e113d78575 - Added todo filter option for Everyone to show all the family's tasks 2024-11-20 20:24:37 +01:00
012a70313b - Allowed selecting date when Repeat type is set to "None"
- Enabled opening the repeat freq picker with click on the Repeat todo icon
2024-11-20 20:04:23 +01:00
cc2a649f8c - Reverted the Add todo press functionality to open the Add todo dialog instead of redirecting to Todos page 2024-11-20 19:27:14 +01:00
81477ed85b fixed refresh, tablet drawer, drawer titles 2024-11-20 00:27:43 +01:00
9f32e81672 supportsTablet 2024-11-19 14:16:46 +01:00
bd890406b1 podfile update 2024-11-19 11:55:27 +01:00
47f035708c notification update 2024-11-19 11:54:16 +01:00
8763be4613 fixed tablet view / spelling 2024-11-18 22:14:00 +01:00
89d00cdead - Fixed issue with events not being mapped correctly when fetched(missing attributes)
- Fixed issue with editing of events because of incorrect attribute name
- Color events created by other user, but assigned to current user in the current user's color
2024-11-18 17:46:01 +01:00
d8e7dc6bf7 - Enabled Add todo button and added navigation to the todos page on click 2024-11-18 17:11:39 +01:00
0ba2cbd65d fixed calendar after tablet merge 2024-11-18 01:43:09 +01:00
662b38f894 Merge branch 'tablet' into dev 2024-11-18 00:43:39 +01:00
f269c9c136 Merge remote-tracking branch 'origin/dev' into dev 2024-11-18 00:17:58 +01:00
eace5da5cb - Implemented saving of location to the event 2024-11-18 00:17:49 +01:00
55d3877c5b Merge branch 'dev' of https://github.com/urosran/cally into dev 2024-11-17 23:53:36 +01:00
111244e178 added household name 2024-11-17 23:53:28 +01:00
c9b796bd40 - Improvements to the todo filter 2024-11-17 22:53:11 +01:00
c1f7113dfc Merge remote-tracking branch 'origin/dev' into dev 2024-11-17 20:24:11 +01:00
f21f70a45d - Fixed infinite rendering in the todos page 2024-11-17 20:24:01 +01:00
9d16e8b301 Merge branch 'dev' of https://github.com/urosran/cally into dev 2024-11-17 20:23:53 +01:00
d7928c799d added calendar events refresh 2024-11-17 20:23:41 +01:00
4fa18a6913 - Reverted useSignUp.ts changes 2024-11-17 19:10:37 +01:00
a95191c890 - Filtering todos 2024-11-17 19:06:50 +01:00
6489bdf237 Merge remote-tracking branch 'origin/main' into dev 2024-11-12 21:42:19 +01:00
3fb9dd0035 add more options to user management 2024-11-12 21:38:03 +01:00
e2aae47c34 - Changed the todo repeat icon 2024-11-10 18:11:22 +01:00
5b80a3ba80 - Added Daily repeat type for the todos and implemented creating and updating this kinds of todos 2024-11-10 18:00:17 +01:00
7da8005fb1 - Added alert if title is empty on todo save 2024-11-10 17:33:47 +01:00
f7909de546 - Added preslected assignee to be the current user when creating todo 2024-11-10 17:25:00 +01:00
844f534e13 ui fixes and date press feature 2024-11-10 15:25:07 +01:00
8ac7f8b2eb notification update 2024-11-05 13:46:29 +01:00
4318f58e22 Merge branch 'dev'
# Conflicts:
#	components/pages/calendar/EventCalendar.tsx
#	components/pages/settings/user_settings_views/MyProfile.tsx
2024-11-05 13:44:42 +01:00
84a974f3f7 Added notifciations 2024-11-04 01:01:59 +01:00
848211c3c8 Merge branch 'dev'
# Conflicts:
#	app/(auth)/_layout.tsx
#	components/pages/settings/user_settings_views/MyProfile.tsx
#	yarn.lock
2024-11-04 00:26:18 +01:00
a577bfb23f cal sync improvements 2024-11-04 00:24:31 +01:00
f1b0bcd32d sync update 2024-11-02 22:27:17 +01:00
8a0370933d sync update 2024-11-02 12:00:49 +01:00
a6b2c4b97f "supportsTablet": false, 2024-11-02 00:28:38 +01:00
e25a4f07ff "supportsTablet": false, 2024-11-02 00:02:54 +01:00
c4e4a4a798 Faimly 2024-11-01 23:45:05 +01:00
18223c5f92 All day event package 2024-11-01 23:17:56 +01:00
87137e7b15 All day event package 2024-11-01 22:02:36 +01:00
61fff87975 Update fetching google calendar, fix babel config 2024-11-01 18:19:56 +01:00
e306f312f4 Update deps 2024-11-01 17:30:57 +01:00
5e82fffa80 Update deps 2024-11-01 12:04:55 +01:00
3e517e3a53 Update deps 2024-11-01 11:52:49 +01:00
a4a0a66a2a Reduce sync 2024-11-01 11:01:28 +01:00
9f37d22d5a Build update 2024-11-01 10:32:56 +01:00
c1252cf092 Dont use enriched events 2024-11-01 10:29:23 +01:00
e965072186 CICD update 2024-11-01 10:06:23 +01:00
0a54d1e576 Bundle update 2024-11-01 09:43:42 +01:00
38586d34ff Legacy peer deps 2024-11-01 08:36:41 +01:00
9ca96b2286 profile deletion added 2024-11-01 05:19:11 +01:00
6e7a3475d1 PKG update 2024-11-01 05:00:54 +01:00
0d2fe33caa PKG update 2024-11-01 04:58:55 +01:00
4f1a96d0f1 PKG update 2024-11-01 04:47:58 +01:00
2605f4622b Merge branch 'dev'
# Conflicts:
#	yarn.lock
2024-11-01 04:46:19 +01:00
241 changed files with 21018 additions and 30858 deletions

View File

@ -25,7 +25,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: npm
cache: yarn
- name: Setup Expo and EAS
uses: expo/expo-github-action@v8
@ -34,7 +34,7 @@ jobs:
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: npm ci --legacy-peer-deps
run: yarn install --immutable
- name: Prebuild, Build and Submit
run: npm run prebuild-build-submit-ios-cicd
run: yarn prebuild-build-submit-ios-cicd

1
.gitignore vendored
View File

@ -21,3 +21,4 @@ expo-env.d.ts
/ios/GoogleService-Info.plist
/ios/cally.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
expo-env.d.ts
./android

6
.idea/git_toolbox_blame.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxBlameSettings">
<option name="version" value="2" />
</component>
</project>

View File

@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="t" enabled="true" level="TEXT ATTRIBUTES" enabled_by_default="true" editorAttributes="CONSIDERATION_ATTRIBUTES" />
</profile>
</component>

6
.idea/jsLinters/eslint.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<option name="fix-on-save" value="true" />
</component>
</project>

85
.idea/jsLinters/jshint.xml generated Normal file
View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JSHintConfiguration" version="2.13.6" use-config-file="false">
<option asi="false" />
<option bitwise="true" />
<option boss="false" />
<option browser="true" />
<option browserify="false" />
<option camelcase="true" />
<option couch="false" />
<option curly="true" />
<option debug="false" />
<option devel="false" />
<option dojo="false" />
<option elision="false" />
<option enforceall="false" />
<option eqeqeq="true" />
<option eqnull="false" />
<option es3="false" />
<option es5="false" />
<option esnext="false" />
<option evil="false" />
<option expr="false" />
<option forin="true" />
<option freeze="true" />
<option funcscope="false" />
<option futurehostile="false" />
<option gcl="false" />
<option globalstrict="false" />
<option immed="false" />
<option iterator="false" />
<option jasmine="false" />
<option jquery="false" />
<option lastsemic="false" />
<option latedef="false" />
<option laxbreak="false" />
<option laxcomma="false" />
<option loopfunc="false" />
<option maxerr="50" />
<option mocha="false" />
<option module="false" />
<option mootools="false" />
<option moz="false" />
<option multistr="false" />
<option newcap="false" />
<option noarg="true" />
<option nocomma="false" />
<option node="false" />
<option noempty="true" />
<option nomen="false" />
<option nonbsp="false" />
<option nonew="true" />
<option nonstandard="false" />
<option notypeof="false" />
<option noyield="false" />
<option onevar="false" />
<option passfail="false" />
<option phantom="false" />
<option plusplus="false" />
<option proto="false" />
<option prototypejs="false" />
<option qunit="false" />
<option quotmark="false" />
<option rhino="false" />
<option scripturl="false" />
<option shadow="false" />
<option shelljs="false" />
<option singleGroups="false" />
<option smarttabs="false" />
<option strict="true" />
<option sub="false" />
<option supernew="false" />
<option trailing="false" />
<option typed="false" />
<option undef="true" />
<option unused="false" />
<option validthis="false" />
<option varstmt="false" />
<option white="false" />
<option withstmt="false" />
<option worker="false" />
<option wsh="false" />
<option yui="false" />
</component>
</project>

12
.idea/material_theme_project_new.xml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-7d3b2185:193a8bd7023:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>

View File

@ -1,5 +1,4 @@
apply plugin: "com.android.application"
apply plugin: 'com.google.gms.google-services'
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
@ -21,12 +20,12 @@ react {
bundleCommand = "export:embed"
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '..'
// root = file("../")
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
// reactNativeDir = file("../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
// codegenDir = file("../node_modules/@react-native/codegen")
// The root of your project, i.e. where "package.json" lives. Default is '../..'
// root = file("../../")
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
// reactNativeDir = file("../../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
// codegenDir = file("../../node_modules/@react-native/codegen")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
@ -58,6 +57,9 @@ react {
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
/* Autolinking */
autolinkLibrariesWithApp()
}
/**
@ -91,9 +93,6 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
manifestPlaceholders = [
appAuthRedirectScheme: "callyplanner"
]
}
signingConfigs {
debug {
@ -122,6 +121,9 @@ android {
useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
}
}
androidResources {
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
// Apply static values from `gradle.properties` to the `android.packagingOptions`
@ -173,7 +175,5 @@ dependencies {
}
}
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
applyNativeModulesAppBuildGradle(project)
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'

View File

@ -15,7 +15,7 @@
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true">
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="default"/>
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color"/>
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
@ -33,12 +33,10 @@
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="myapp"/>
<data android:scheme="callyplanner"/>
<data android:scheme="com.cally.app"/>
<data android:scheme="exp+cally"/>
<data android:scheme="callyplanner"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false"/>
</application>
</manifest>

View File

@ -1,4 +1,5 @@
package com.cally.app
import expo.modules.splashscreen.SplashScreenManager
import android.os.Build
import android.os.Bundle
@ -15,7 +16,10 @@ class MainActivity : ReactActivity() {
// Set the theme to AppTheme BEFORE onCreate to support
// coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen.
setTheme(R.style.AppTheme);
// setTheme(R.style.AppTheme);
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
SplashScreenManager.registerOnActivity(this)
// @generated end expo-splashscreen
super.onCreate(null)
}

View File

@ -10,6 +10,7 @@ import com.facebook.react.ReactPackage
import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import expo.modules.ApplicationLifecycleDispatcher
@ -21,9 +22,10 @@ class MainApplication : Application(), ReactApplication {
this,
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
return PackageList(this).packages
return packages
}
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
@ -40,7 +42,7 @@ class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View File

@ -1,3 +1,6 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
<item>
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
</item>
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1,6 +1,6 @@
<resources>
<color name="splashscreen_background">#ffffff</color>
<color name="iconBackground">#ffffff</color>
<color name="iconBackground">#FFFFFF</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#ffffff</color>
<color name="notification_icon_color">#ffffff</color>

View File

@ -1,5 +1,5 @@
<resources>
<string name="app_name">"Cally "</string>
<string name="app_name">\"Cally \"</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>

View File

@ -11,7 +11,9 @@
<item name="android:textColorHint">#c8c8c8</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Theme.App.SplashScreen" parent="AppTheme">
<item name="android:windowBackground">@drawable/splashscreen</item>
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
</resources>

View File

@ -2,11 +2,11 @@
buildscript {
ext {
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '34')
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.23'
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.24'
ndkVersion = "26.1.10909125"
}
@ -16,10 +16,10 @@ buildscript {
}
dependencies {
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.2'
classpath 'com.google.gms:google-services:4.4.1'
classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin')
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
classpath('com.google.gms:google-services:4.4.2')
}
}
@ -41,7 +41,3 @@ allprojects {
maven { url 'https://www.jitpack.io' }
}
}
// @generated begin expo-camera-import - expo prebuild (DO NOT MODIFY) sync-f244f4f3d8bf7229102e8f992b525b8602c74770
def expoCameraMavenPath = new File(["node", "--print", "require.resolve('expo-camera/package.json')"].execute(null, rootDir).text.trim(), "../android/maven")
allprojects { repositories { maven { url(expoCameraMavenPath) } } }
// @generated end expo-camera-import

View File

@ -22,9 +22,6 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Enable AAPT2 PNG crunching
android.enablePngCrunchInReleaseBuilds=true

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

7
android/gradlew vendored Executable file → Normal file
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

2
android/gradlew.bat vendored
View File

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################

View File

@ -1,3 +1,26 @@
pluginManagement {
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile().toString())
}
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
ex.autolinkLibrariesFromCommand()
} else {
def command = [
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
'react-native-config',
'--json',
'--platform',
'android'
].toList()
ex.autolinkLibrariesFromCommand(command)
}
}
rootProject.name = 'Cally '
dependencyResolutionManagement {
@ -11,8 +34,5 @@ dependencyResolutionManagement {
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
useExpoModules()
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
applyNativeModulesSettingsGradle(settings)
include ':app'
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile())

View File

@ -4,6 +4,7 @@
"slug": "cally",
"version": "1.0.0",
"orientation": "portrait",
"owner": "tomira",
"icon": "./assets/images/icon.png",
"scheme": "callyplanner",
"userInterfaceStyle": "light",
@ -16,16 +17,19 @@
"supportsTablet": true,
"bundleIdentifier": "com.cally.app",
"googleServicesFile": "./ios/GoogleService-Info.plist",
"buildNumber": "60",
"usesAppleSignIn": true
"buildNumber": "100",
"usesAppleSignIn": true,
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
"backgroundColor": "#FFFFFF"
},
"package": "com.cally.app",
"googleServicesFile": "./android/app/google-services.json",
"googleServicesFile": "./google-services.json",
"permissions": [
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO"

View File

@ -1,286 +1,362 @@
import React, { useEffect } from "react";
import { Drawer } from "expo-router/drawer";
import { useSignOut } from "@/hooks/firebase/useSignOut";
import React, {memo, useCallback, useMemo} from "react";
import {Drawer} from "expo-router/drawer";
import {
DrawerContentScrollView,
DrawerItem,
DrawerItemList,
DrawerContentComponentProps,
DrawerContentScrollView,
DrawerNavigationOptions,
DrawerNavigationProp
} from "@react-navigation/drawer";
import { Button, View, Text, ButtonSize } from "react-native-ui-lib";
import { Dimensions, ImageBackground, StyleSheet } from "react-native";
import Feather from "@expo/vector-icons/Feather";
import {ImageBackground, Pressable, StyleSheet} from "react-native";
import {Button, ButtonSize, Text, View} from "react-native-ui-lib";
import * as Device from "expo-device";
import {DeviceType} from "expo-device";
import {useSetAtom} from "jotai";
import {Ionicons} from "@expo/vector-icons";
import {ParamListBase, RouteProp, Theme} from '@react-navigation/native';
import {useSignOut} from "@/hooks/firebase/useSignOut";
import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader";
import DrawerButton from "@/components/shared/DrawerButton";
import {
AntDesign,
FontAwesome6,
MaterialCommunityIcons,
Octicons,
} from "@expo/vector-icons";
import MenuIcon from "@/assets/svgs/MenuIcon";
import { router } from "expo-router";
import DrawerIcon from "@/assets/svgs/DrawerIcon";
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
import ViewSwitch from "@/components/pages/(tablet_pages)/ViewSwitch";
import { useAtom, useSetAtom } from "jotai";
import {
isFamilyViewAtom,
settingsPageIndex,
toDosPageIndex,
userSettingsView,
} from "@/components/pages/calendar/atoms";
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
import Constants from "expo-constants";
import * as Device from "expo-device";
import { DeviceType } from "expo-device";
import {
isFamilyViewAtom,
settingsPageIndex,
toDosPageIndex,
userSettingsView,
} from "@/components/pages/calendar/atoms";
import ViewSwitch from "@/components/pages/(tablet_pages)/ViewSwitch";
export default function TabLayout() {
const { mutateAsync: signOut } = useSignOut();
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
const setPageIndex = useSetAtom(settingsPageIndex);
const setUserView = useSetAtom(userSettingsView);
const setToDosIndex = useSetAtom(toDosPageIndex);
const isTablet = Device.deviceType === DeviceType.TABLET;
return (
<Drawer
initialRouteName={"index"}
detachInactiveScreens
screenOptions={({ navigation }) => ({
headerShown: true,
headerRight: () =>
Device.deviceType === DeviceType.TABLET ? (
<ViewSwitch navigation={navigation} />
) : (
<></>
),
drawerStyle: {
width: "90%",
backgroundColor: "#f9f8f7",
height: "100%",
},
})}
drawerContent={(props) => {
return (
<DrawerContentScrollView {...props} style={{}}>
<View centerV marginH-30 marginT-20 marginB-20 row>
<ImageBackground
source={require("../../assets/images/splash.png")}
style={{
backgroundColor: "transparent",
height: 51.43,
aspectRatio: 1,
marginRight: 8,
}}
/>
<Text style={styles.title}>Welcome to Cally</Text>
</View>
<View
style={{
flexDirection: "row",
paddingHorizontal: 30,
}}
>
<View style={{ flex: 1, paddingRight: 5 }}>
<DrawerButton
title={"Calendar"}
color="rgb(7, 184, 199)"
bgColor={"rgb(231, 248, 250)"}
pressFunc={() => {
props.navigation.navigate("calendar");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavCalendarIcon />}
/>
<DrawerButton
color="#50be0c"
title={"Groceries"}
bgColor={"#eef9e7"}
pressFunc={() => {
props.navigation.navigate("grocery");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavGroceryIcon />}
/>
<DrawerButton
color="#ea156d"
title={"Feedback"}
bgColor={"#fdedf4"}
pressFunc={() => {
props.navigation.navigate("feedback");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<FeedbackNavIcon />}
/>
</View>
<View style={{ flex: 1 }}>
{/*<DrawerButton
color="#fd1775"
title={"My Reminders"}
bgColor={"#ffe8f2"}
pressFunc={() => props.navigation.navigate("reminders")}
icon={
<FontAwesome6
name="clock-rotate-left"
size={28}
color="#fd1775"
/>
}
/>*/}
<DrawerButton
color="#8005eb"
title={"To Do's"}
bgColor={"#f3e6fd"}
pressFunc={() => {
props.navigation.navigate("todos");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavToDosIcon />}
/>
<DrawerButton
color="#e0ca03"
title={"Brain Dump"}
bgColor={"#fffacb"}
pressFunc={() => {
props.navigation.navigate("brain_dump");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavBrainDumpIcon />}
/>
{/*<DrawerItem label="Logout" onPress={() => signOut()} />*/}
</View>
</View>
<Button
onPress={() => {
props.navigation.navigate("settings");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
label={"Manage Settings"}
labelStyle={styles.label}
iconSource={() => (
<View
backgroundColor="#ededed"
width={60}
height={60}
style={{ borderRadius: 50 }}
marginR-10
centerV
centerH
>
<NavSettingsIcon />
</View>
)}
backgroundColor="white"
color="#464039"
paddingV-30
marginH-30
marginB-10
borderRadius={18.55}
style={{ elevation: 0 }}
/>
type DrawerParamList = {
index: undefined;
calendar: undefined;
brain_dump: undefined;
settings: undefined;
grocery: undefined;
reminders: undefined;
todos: undefined;
notifications: undefined;
feedback: undefined;
};
<Button
size={ButtonSize.large}
marginH-30
marginT-12
paddingV-15
style={{
backgroundColor: "transparent",
borderWidth: 1.3,
borderColor: "#fd1775",
}}
label="Sign out of Cally"
color="#fd1775"
labelStyle={styles.signOut}
onPress={() => signOut()}
/>
</DrawerContentScrollView>
);
}}
>
<Drawer.Screen
name="index"
options={{
drawerLabel: "Calendar",
title: "Calendar",
}}
/>
<Drawer.Screen
name="calendar"
options={{
drawerLabel: "Calendar",
title: "Calendar",
drawerItemStyle: { display: "none" },
}}
/>
<Drawer.Screen
name="brain_dump"
options={{
drawerLabel: "Brain Dump",
title: "Brain Dump",
}}
/>
<Drawer.Screen
name="settings"
options={{
drawerLabel: "Settings",
title: "Settings",
}}
/>
<Drawer.Screen
name="grocery"
options={{
drawerLabel: "Grocery",
title: "Grocery",
}}
/>
<Drawer.Screen
name="reminders"
options={{
drawerLabel: "Reminders",
title: "Reminders",
}}
/>
<Drawer.Screen
name="todos"
options={{
drawerLabel: "To-Do",
title: "To-Dos",
}}
/>
<Drawer.Screen
name="feedback"
options={{ drawerLabel: "Feedback", title: "Feedback" }}
/>
</Drawer>
);
type DrawerScreenNavigationProp = DrawerNavigationProp<DrawerParamList>;
interface DrawerButtonConfig {
id: string;
title: string;
color: string;
bgColor: string;
icon: React.FC;
route: keyof DrawerParamList;
}
const styles = StyleSheet.create({
signOut: { fontFamily: "Poppins_500Medium", fontSize: 15 },
label: { fontFamily: "Poppins_400Medium", fontSize: 15 },
title: {
fontSize: 26.13,
fontFamily: "Manrope_600SemiBold",
color: "#262627",
},
const DRAWER_BUTTONS: DrawerButtonConfig[] = [
{
id: 'calendar',
title: 'Calendar',
color: 'rgb(7, 184, 199)',
bgColor: 'rgb(231, 248, 250)',
icon: NavCalendarIcon,
route: 'calendar'
},
{
id: 'grocery',
title: 'Groceries',
color: '#50be0c',
bgColor: '#eef9e7',
icon: NavGroceryIcon,
route: 'grocery'
},
{
id: 'feedback',
title: 'Feedback',
color: '#ea156d',
bgColor: '#fdedf4',
icon: FeedbackNavIcon,
route: 'feedback'
},
{
id: 'todos',
title: 'To Dos',
color: '#8005eb',
bgColor: '#f3e6fd',
icon: NavToDosIcon,
route: 'todos'
},
{
id: 'brain_dump',
title: 'Brain Dump',
color: '#e0ca03',
bgColor: '#fffacb',
icon: NavBrainDumpIcon,
route: 'brain_dump'
},
{
id: 'notifications',
title: 'Notifications',
color: '#ffa200',
bgColor: '#ffdda1',
icon: () => <Ionicons name="notifications-outline" size={24} color="#ffa200"/>,
route: 'notifications'
}
];
interface DrawerContentProps {
props: DrawerContentComponentProps;
}
const DrawerContent: React.FC<DrawerContentProps> = ({props}) => {
const {mutateAsync: signOut} = useSignOut();
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
const setPageIndex = useSetAtom(settingsPageIndex);
const setUserView = useSetAtom(userSettingsView);
const setToDosIndex = useSetAtom(toDosPageIndex);
const handleNavigation = useCallback((route: keyof DrawerParamList) => {
props.navigation.navigate(route);
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}, [props.navigation, setPageIndex, setToDosIndex, setUserView, setIsFamilyView]);
const renderDrawerButtons = () => {
const midPoint = Math.ceil(DRAWER_BUTTONS.length / 2);
const leftButtons = DRAWER_BUTTONS.slice(0, midPoint);
const rightButtons = DRAWER_BUTTONS.slice(midPoint);
return (
<View row paddingH-30>
<View flex-1 paddingR-5>
{leftButtons.map(button => (
<DrawerButton
key={button.id}
title={button.title}
color={button.color}
bgColor={button.bgColor}
pressFunc={() => handleNavigation(button.route)}
icon={<button.icon/>}
/>
))}
</View>
<View flex-1>
{rightButtons.map(button => (
<DrawerButton
key={button.id}
title={button.title}
color={button.color}
bgColor={button.bgColor}
pressFunc={() => handleNavigation(button.route)}
icon={<button.icon/>}
/>
))}
</View>
</View>
);
};
return (
<DrawerContentScrollView {...props}>
<View centerV marginH-30 marginT-20 marginB-20 row>
<ImageBackground
source={require("../../assets/images/splash.png")}
style={styles.logo}
/>
<Text style={styles.title}>Welcome to Cally</Text>
</View>
{renderDrawerButtons()}
<Button
onPress={() => handleNavigation('settings')}
label="Manage Settings"
labelStyle={styles.label}
iconSource={() => (
<View style={styles.settingsIcon}>
<NavSettingsIcon/>
</View>
)}
backgroundColor="white"
color="#464039"
paddingV-30
marginH-30
borderRadius={18.55}
style={{elevation: 0}}
/>
<Button
size={ButtonSize.large}
style={styles.signOutButton}
label="Sign out of Cally"
color="#fd1775"
labelStyle={styles.signOut}
onPress={() => signOut()}
/>
</DrawerContentScrollView>
);
};
interface HeaderRightProps {
route: RouteProp<DrawerParamList>;
navigation: DrawerScreenNavigationProp;
}
const HeaderRight: React.FC<HeaderRightProps> = memo(({route, navigation}) => {
const showViewSwitch = ["calendar", "todos", "index"].includes(route.name);
const isCalendarPage = ["calendar", "index"].includes(route.name);
if (!isTablet || !showViewSwitch) {
return isCalendarPage ? <CalendarHeader/> : null;
}
return (
<View marginR-16 row centerV>
{isTablet && isCalendarPage && (
<View flex-1 center>
<CalendarHeader/>
</View>
)}
<ViewSwitch navigation={navigation}/>
</View>
)
});
interface DrawerScreen {
name: keyof DrawerParamList;
title: string;
hideInDrawer?: boolean;
}
const DRAWER_SCREENS: DrawerScreen[] = [
{name: 'index', title: 'Calendar'},
{name: 'calendar', title: 'Calendar', hideInDrawer: true},
{name: 'brain_dump', title: 'Brain Dump'},
{name: 'settings', title: 'Settings'},
{name: 'grocery', title: 'Groceries'},
{name: 'reminders', title: 'Reminders'},
{name: 'todos', title: isTablet ? 'Family To Dos' : 'To Dos'},
{name: 'notifications', title: 'Notifications'},
{name: 'feedback', title: 'Feedback'}
];
const TabLayout: React.FC = () => {
const screenOptions = useMemo(() => {
return ({route, navigation}: {
route: RouteProp<ParamListBase, string>;
navigation: DrawerNavigationProp<ParamListBase, string>;
theme: Theme;
}): DrawerNavigationOptions => ({
lazy: true,
headerShown: true,
headerTitleAlign: "left",
headerTitle: ({children}) => {
const isCalendarRoute = ["calendar", "index"].includes(route.name);
if (isCalendarRoute) return null;
return (
<View flexG centerV paddingL-10>
<Text style={styles.headerTitle}>
{children}
</Text>
</View>
);
},
headerLeft: () => (
<Pressable
onPress={() => navigation.toggleDrawer()}
hitSlop={{top: 10, bottom: 10, left: 10, right: 10}}
style={({pressed}) => [styles.drawerTrigger, {opacity: pressed ? 0.4 : 1}]}
>
<DrawerIcon/>
</Pressable>
),
headerRight: () => <HeaderRight
route={route as RouteProp<DrawerParamList>}
navigation={navigation as DrawerNavigationProp<DrawerParamList>}
/>,
drawerStyle: styles.drawer,
});
}, []);
return (
<Drawer
initialRouteName="index"
detachInactiveScreens
screenOptions={screenOptions}
drawerContent={(props) => <DrawerContent props={props}/>}
>
{DRAWER_SCREENS.map(screen => (
<Drawer.Screen
key={screen.name}
name={screen.name}
options={{
drawerLabel: screen.title,
title: screen.title,
...(screen.hideInDrawer && {drawerItemStyle: {display: 'none'}}),
}}
/>
))}
</Drawer>
);
};
const styles = StyleSheet.create({
drawer: {
width: isTablet ? "30%" : "90%",
backgroundColor: "#f9f8f7",
height: "100%",
},
drawerTrigger: {
marginLeft: 16,
},
headerTitle: {
fontFamily: "Manrope_600SemiBold",
fontSize: isTablet ? 22 : 17,
},
logo: {
backgroundColor: "transparent",
height: 51.43,
aspectRatio: 1,
marginRight: 8,
},
settingsIcon: {
backgroundColor: "#ededed",
width: 60,
height: 60,
borderRadius: 50,
marginRight: 10,
justifyContent: 'center',
alignItems: 'center',
},
signOutButton: {
marginTop: 50,
marginHorizontal: 10,
paddingVertical: 15,
backgroundColor: "transparent",
borderWidth: 1.3,
borderColor: "#fd1775",
},
signOut: {
fontFamily: "Poppins_500Medium",
fontSize: 15,
},
label: {
fontFamily: "Poppins_400Medium",
fontSize: 15,
},
title: {
fontSize: 26.13,
fontFamily: "Manrope_600SemiBold",
color: "#262627",
},
});
export default TabLayout;

View File

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

View File

@ -1,18 +1,92 @@
import React from "react";
import React, {useEffect} from "react";
import {RefreshControl, ScrollView, View} from "react-native";
import CalendarPage from "@/components/pages/calendar/CalendarPage";
import { View } from "react-native-ui-lib";
import TabletCalendarPage from "@/components/pages/(tablet_pages)/calendar/TabletCalendarPage";
import { DeviceType } from "expo-device";
import * as Device from "expo-device";
import {DeviceType} from "expo-device";
import {useCalSync} from "@/hooks/useCalSync";
import {colorMap} from "@/constants/colorMap";
import {useSetAtom} from "jotai";
import {selectedUserAtom} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
export default function Screen() {
return (
<View style={{ backgroundColor: "white" }}>
{Device.deviceType === DeviceType.TABLET ? (
<TabletCalendarPage />
) : (
<CalendarPage />
)}
</View>
);
}
const isTablet = Device.deviceType === DeviceType.TABLET;
const {resyncAllCalendars, isSyncing} = useCalSync();
const setSelectedUser = useSetAtom(selectedUserAtom);
const {profileData} = useAuthContext()
useEffect(() => {
if (!isTablet && profileData) setSelectedUser({
uid: profileData.uid!,
firstName: profileData.firstName,
lastName: profileData.lastName,
eventColor: profileData?.eventColor!
})
}, [])
const onRefresh = React.useCallback(async () => {
try {
await resyncAllCalendars();
} catch (error) {
console.error("Refresh failed:", error);
}
}, [resyncAllCalendars]);
const refreshControl = (
<RefreshControl
colors={[
colorMap.pink,
colorMap.green,
colorMap.orange,
colorMap.purple,
colorMap.teal,
]}
tintColor={colorMap.pink}
progressBackgroundColor="white"
refreshing={isSyncing}
onRefresh={onRefresh}
style={isTablet ? {
position: "absolute",
left: "50%",
transform: [{translateX: -20}],
} : undefined}
/>
);
if (isTablet) {
return (
<View style={{flex: 1}}>
<View style={{flex: 1, zIndex: 0}}>
<TabletCalendarPage/>
</View>
<ScrollView
style={{
position: "absolute",
top: 0,
left: "15%",
height: "9%",
width: "62%",
zIndex: 50,
backgroundColor: "transparent",
}}
contentContainerStyle={{
flex: 1,
justifyContent: "center",
paddingRight: 200,
}}
bounces={true}
showsVerticalScrollIndicator={false}
pointerEvents={isSyncing ? "auto" : "none"}
/>
</View>
);
}
return (
<View style={{flex: 1}}>
<CalendarPage/>
</View>
);
}

View File

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

View File

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

View File

@ -1,25 +1,16 @@
import TabletChoresPage from "@/components/pages/(tablet_pages)/chores/TabletChoresPage";
import AddChore from "@/components/pages/todos/AddChore";
import ProgressCard from "@/components/pages/todos/ProgressCard";
import ToDoItem from "@/components/pages/todos/ToDoItem";
import ToDosList from "@/components/pages/todos/ToDosList";
import ToDosPage from "@/components/pages/todos/ToDosPage";
import HeaderTemplate from "@/components/shared/HeaderTemplate";
import { useAuthContext } from "@/contexts/AuthContext";
import { ToDosContextProvider, useToDosContext } from "@/contexts/ToDosContext";
import { AntDesign } from "@expo/vector-icons";
import { ScrollView } from "react-native-gesture-handler";
import { Button, ButtonSize, View, Text, Constants } from "react-native-ui-lib";
import {ToDosContextProvider} from "@/contexts/ToDosContext";
import * as Device from "expo-device";
export default function Screen() {
return (
<ToDosContextProvider>
{Device.deviceType === Device.DeviceType.TABLET ? (
<TabletChoresPage />
) : (
<ToDosPage />
)}
</ToDosContextProvider>
);
return (
<ToDosContextProvider>
{Device.deviceType === Device.DeviceType.TABLET ? (
<TabletChoresPage/>
) : (
<ToDosPage/>
)}
</ToDosContextProvider>
);
}

View 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",
},
});

View File

@ -1,17 +1,24 @@
import {SafeAreaView} from "react-native-safe-area-context";
import {Button, Text, View} from "react-native-ui-lib";
import React from "react";
import React, { useEffect } from "react";
import {useCalSync} from "@/hooks/useCalSync";
import GoogleIcon from "@/assets/svgs/GoogleIcon";
import AppleIcon from "@/assets/svgs/AppleIcon";
import OutlookIcon from "@/assets/svgs/OutlookIcon";
import {useAuthContext} from "@/contexts/AuthContext";
import {StyleSheet} from "react-native";
import { useGetHouseholdName } from "@/hooks/firebase/useGetHouseholdName";
export default function Screen() {
const {profileData, setRedirectOverride} = useAuthContext()
const {handleStartGoogleSignIn, handleAppleSignIn, handleMicrosoftSignIn} = useCalSync()
const {data: householdName, refetch} = useGetHouseholdName(profileData?.familyId);
useEffect(() => {
refetch();
}, [profileData?.familyId])
const hasSomeCalendarsSynced =
!!profileData?.appleAccounts || !!profileData?.microsoftAccounts || !!profileData?.googleAccounts
@ -19,6 +26,9 @@ export default function Screen() {
<SafeAreaView style={{flex: 1}}>
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
<View gap-13 width={"100%"} marginB-20>
{householdName && <Text style={{fontSize: 25, fontFamily: 'Manrope_600SemiBold'}}>
You Joined The {householdName} Household
</Text>}
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}>
Let's get started!
</Text>

View File

@ -1,169 +1,229 @@
import {SafeAreaView} from "react-native-safe-area-context";
import {Button, Colors, Dialog, LoaderScreen, Text, View} from "react-native-ui-lib";
import React, {useCallback, useState} from "react";
import {useRouter} from "expo-router";
import { SafeAreaView } from "react-native-safe-area-context";
import {
Button,
Colors,
Dialog,
LoaderScreen,
Text,
View,
} from "react-native-ui-lib";
import React, { useCallback, useEffect, useState } from "react";
import { useRouter } from "expo-router";
import QRIcon from "@/assets/svgs/QRIcon";
import {Camera, CameraView} from "expo-camera";
import {useLoginWithQrCode} from "@/hooks/firebase/useLoginWithQrCode";
import {useAuthContext} from "@/contexts/AuthContext";
import { Camera, CameraView } from "expo-camera";
import { useLoginWithQrCode } from "@/hooks/firebase/useLoginWithQrCode";
import { useAuthContext } from "@/contexts/AuthContext";
import debounce from "debounce";
import * as Device from "expo-device";
import { DeviceType } from "expo-device";
import { Dimensions } from "react-native";
export default function Screen() {
const router = useRouter()
const {setRedirectOverride} = useAuthContext()
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
const router = useRouter();
const { setRedirectOverride } = useAuthContext();
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
const {mutateAsync: signInWithQrCode, isLoading} = useLoginWithQrCode();
const isTablet: boolean = Device.deviceType === DeviceType.TABLET;
const [isPortrait, setIsPortrait] = useState(() => {
const dim = Dimensions.get('screen');
return dim.height >= dim.width;
});
useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
setIsPortrait(screen.height >= screen.width);
});
return () => subscription.remove();
}, []);
const debouncedRouterReplace = useCallback(
debounce(() => {
router.push("/(unauth)/cal_sync");
}, 300),
[]
);
const handleQrCodeScanned = async ({data}: { data: string }) => {
setShowCameraDialog(false);
setRedirectOverride(true);
try {
await signInWithQrCode({userId: data});
debouncedRouterReplace()
} catch (err) {
console.log(err)
}
};
const getCameraPermissions = async (callback: () => void) => {
const {status} = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === "granted");
if (status === "granted") {
callback();
}
};
const handleOpenQrCodeDialog = () => {
getCameraPermissions(() => setShowCameraDialog(true));
const getTopPadding = () => {
if (Device.deviceType === DeviceType.TABLET) {
return isPortrait ? "50%" : "15%";
}
return "20%"; // non-tablet case, regardless of orientation
};
return (
<SafeAreaView style={{flex: 1}}>
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
<View center>
<Text style={{fontSize: 30, fontFamily: 'Manrope_600SemiBold'}}>
Get started with Cally
</Text>
</View>
const { mutateAsync: signInWithQrCode, isLoading } = useLoginWithQrCode();
<View width={"100%"} gap-30>
<View>
<Button
label="Scan QR Code"
marginT-50
labelStyle={{
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
marginLeft: 10
}}
iconSource={() => <QRIcon color={"#07B8C7"}/>}
onPress={handleOpenQrCodeDialog}
style={{height: 50}}
color={Colors.black}
backgroundColor={Colors.white}
/>
{/* GOOGLE LOGIN HERE */}
</View>
const debouncedRouterReplace = useCallback(
debounce(() => {
router.push("/(unauth)/birthday_page");
}, 300),
[]
);
<View row center gap-20>
<View flexG style={{backgroundColor: "#E2E2E2", height: 2}}/>
<Text style={{fontSize: 16, fontFamily: 'PlusJakartaSans_300Light', color: "#7A7A7A"}}>
or
</Text>
<View flexG style={{backgroundColor: "#E2E2E2", height: 2}}/>
</View>
const handleQrCodeScanned = async ({ data }: { data: string }) => {
setShowCameraDialog(false);
setRedirectOverride(true);
try {
await signInWithQrCode({ userId: data });
debouncedRouterReplace();
} catch (err) {
console.log(err);
}
};
<View>
<Button
label="Contine with Email"
labelStyle={{
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
marginLeft: 10
}}
onPress={() => router.push("/(unauth)/sign_up")}
style={{height: 50, borderStyle: "solid", borderColor: "#E2E2E2", borderWidth: 2}}
color={Colors.black}
backgroundColor={"transparent"}
/>
</View>
</View>
const getCameraPermissions = async (callback: () => void) => {
const { status } = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === "granted");
if (status === "granted") {
callback();
}
};
const handleOpenQrCodeDialog = () => {
getCameraPermissions(() => setShowCameraDialog(true));
};
<View flexG/>
return (
<SafeAreaView style={{ flex: 1, alignItems: "center" }}>
<View
style={{
flex: 1,
padding: 21,
paddingBottom: 45,
paddingTop: isLoading ? "20%" : getTopPadding(),
width: isTablet ? 629 : '100%'
}}
>
<View center>
<Text style={{ fontSize: 30, fontFamily: "Manrope_600SemiBold" }}>
Get started with Cally
</Text>
</View>
<View row centerH gap-5>
<Text style={{
fontFamily: "PlusJakartaSans_300Light",
fontSize: 16,
color: "#484848"
}} center>
Already have an account?
</Text>
<View width={"100%"} gap-30>
<View>
<Button
label="Scan QR Code"
marginT-50
labelStyle={{
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
marginLeft: 10,
}}
iconSource={() => <QRIcon color={"#07B8C7"} />}
onPress={handleOpenQrCodeDialog}
style={{ height: 50 }}
color={Colors.black}
backgroundColor={Colors.white}
/>
{/* GOOGLE LOGIN HERE */}
</View>
<Button
label="Log in"
link
onPress={() => router.push("/(unauth)/sign_in")}
labelStyle={[
{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
color: "#919191",
},
{fontSize: 16, textDecorationLine: "none", color: "#fd1775"},
]}
/>
</View>
</View>
{/* Legacy, move into separate component */}
{/* Camera Dialog */}
<Dialog
visible={showCameraDialog}
onDismiss={() => setShowCameraDialog(false)}
bottom
width="100%"
height="70%"
containerStyle={{padding: 15, backgroundColor: "white"}}
<View row center gap-20>
<View flexG style={{ backgroundColor: "#E2E2E2", height: 2 }} />
<Text
style={{
fontSize: 16,
fontFamily: "PlusJakartaSans_300Light",
color: "#7A7A7A",
}}
>
<Text center style={{fontSize: 16}} marginB-15>
Scan a QR code presented from your family member of provider.
</Text>
{hasPermission === null ? (
<Text>Requesting camera permissions...</Text>
) : !hasPermission ? (
<Text>No access to camera</Text>
) : (
<CameraView
style={{flex: 1, borderRadius: 15}}
onBarcodeScanned={handleQrCodeScanned}
barcodeScannerSettings={{
barcodeTypes: ["qr"],
}}
/>
)}
<Button
label="Cancel"
onPress={() => setShowCameraDialog(false)}
backgroundColor="#fd1775"
style={{margin: 10, marginBottom: 30}}
/>
</Dialog>
or
</Text>
<View flexG style={{ backgroundColor: "#E2E2E2", height: 2 }} />
</View>
<View>
<Button
label="Continue with Email"
labelStyle={{
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
marginLeft: 10,
}}
onPress={() => router.push("/(unauth)/sign_up")}
style={{
height: 50,
borderStyle: "solid",
borderColor: "#E2E2E2",
borderWidth: 2,
}}
color={Colors.black}
backgroundColor={"transparent"}
/>
</View>
</View>
{isLoading && (
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white} color={Colors.grey40}/>
)}
</SafeAreaView>
)
{isTablet ? (
<View marginT-30 />
) : (
<View flexG />
)}
<View row centerH gap-5>
<Text
style={{
fontFamily: "PlusJakartaSans_300Light",
fontSize: 16,
color: "#484848",
}}
center
>
Already have an account?
</Text>
<Button
label="Log in"
link
onPress={() => router.push("/(unauth)/sign_in")}
labelStyle={[
{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
color: "#919191",
},
{ fontSize: 16, textDecorationLine: "none", color: "#fd1775" },
]}
/>
</View>
</View>
{/* Legacy, move into separate component */}
{/* Camera Dialog */}
<Dialog
visible={showCameraDialog}
onDismiss={() => setShowCameraDialog(false)}
bottom
width="100%"
height="70%"
containerStyle={{ padding: 15, backgroundColor: "white" }}
>
<Text center style={{ fontSize: 16 }} marginB-15>
Scan a QR code presented from your family member of provider.
</Text>
{hasPermission === null ? (
<Text>Requesting camera permissions...</Text>
) : !hasPermission ? (
<Text>No access to camera</Text>
) : (
<CameraView
style={{ flex: 1, borderRadius: 15 }}
onBarcodeScanned={handleQrCodeScanned}
barcodeScannerSettings={{
barcodeTypes: ["qr"],
}}
/>
)}
<Button
label="Cancel"
onPress={() => setShowCameraDialog(false)}
backgroundColor="#fd1775"
style={{ margin: 10, marginBottom: 30 }}
/>
</Dialog>
{isLoading && (
<LoaderScreen
overlay
message={"Signing in..."}
backgroundColor={Colors.white}
color={Colors.grey40}
/>
)}
</SafeAreaView>
);
}

View File

@ -0,0 +1,87 @@
import { SafeAreaView } from "react-native-safe-area-context";
import { Button, Text, View, TextField } from "react-native-ui-lib";
import React, { useState } from "react";
import { useRouter } from "expo-router";
import { StyleSheet } from "react-native";
import { useAuthContext } from "@/contexts/AuthContext";
import { useUpdateHouseholdName } from "@/hooks/firebase/useUpdateHouseholdName";
export default function NewHouseholdScreen() {
const router = useRouter();
const { user, profileData } = useAuthContext();
const [householdName, setHouseholdName] = useState("");
const { mutateAsync: newHousehold } = useUpdateHouseholdName();
const handleContinue = async () => {
try {
if(profileData?.familyId)
newHousehold({familyId: profileData?.familyId, name: householdName}).then(() => router.push("/(unauth)/cal_sync"));
} catch (error) {
console.error("Error saving household name:", error);
}
};
return (
<SafeAreaView style={{ flex: 1 }}>
<View
style={{
flex: 1,
padding: 21,
paddingBottom: 45,
paddingTop: "20%",
alignItems: "center",
}}
>
<View gap-13 width={"100%"} marginB-20>
<Text style={{ fontSize: 40, fontFamily: "Manrope_600SemiBold" }}>
Name your household
</Text>
<Text color={"#919191"} style={{ fontSize: 20 }}>
Give your family group a unique name!
</Text>
</View>
<View width={"100%"} flexG>
<TextField
value={householdName}
onChangeText={setHouseholdName}
placeholder="Enter household name"
style={styles.textfield}
textAlign="center"
/>
</View>
<View flexG />
<View width={"100%"}>
<Button
label="Continue"
onPress={handleContinue}
style={{
height: 50,
}}
backgroundColor="#fd1775"
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
/>
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
textfield: {
backgroundColor: "white",
marginVertical: 100,
padding: 30,
height: 44,
borderRadius: 50,
fontFamily: "PlusJakartaSans_300Light",
fontSize: 15,
color: "#919191",
alignContent: "center",
},
});

View File

@ -2,6 +2,8 @@ import {SafeAreaView} from "react-native-safe-area-context";
import {Button, Image, Text, View} from "react-native-ui-lib";
import React from "react";
import {useRouter} from "expo-router";
import * as Device from "expo-device";
import { DeviceType } from "expo-device";
export default function Screen() {
const router = useRouter()
@ -25,7 +27,7 @@ export default function Screen() {
<View flexG/>
<View width={"100%"}>
<View width={"100%"} centerH>
<Button
label="Continue"
marginT-50
@ -34,7 +36,7 @@ export default function Screen() {
fontSize: 16,
}}
onPress={() => router.push("/(unauth)/get_started")}
style={{height: 50}}
style={{height: 50, width: Device.deviceType === DeviceType.TABLET ? 629 : "100%"}}
backgroundColor="#fd1775"
/>
</View>

View File

@ -50,28 +50,29 @@ import {Stack} from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import "react-native-reanimated";
import {AuthContextProvider} from "@/contexts/AuthContext";
import {QueryClient, QueryClientProvider} from "react-query";
import {TextProps, ThemeManager, Toast, Typography,} from "react-native-ui-lib";
import {Platform} from 'react-native';
import KeyboardManager from 'react-native-keyboard-manager';
import {enableScreens} from 'react-native-screens';
import {enableScreens, enableFreeze} from 'react-native-screens';
import {PersistQueryClientProvider} from "@/contexts/PersistQueryClientProvider";
import auth from "@react-native-firebase/auth";
import firestore from '@react-native-firebase/firestore';
import functions from '@react-native-firebase/functions';
enableScreens(true)
enableFreeze(true)
SplashScreen.preventAutoHideAsync();
const queryClient = new QueryClient();
if (Platform.OS === 'ios') {
KeyboardManager.setEnable(true);
KeyboardManager.setToolbarPreviousNextButtonEnable(true);
}
if (__DEV__) {
// functions().useEmulator("localhost", 5001);
// firestore().useEmulator("localhost", 5471);
// auth().useEmulator("http://localhost:9099");
// functions().useEmulator("localhost", 5001);
// firestore().useEmulator("localhost", 5471);
// auth().useEmulator("http://localhost:9099");
}
type TextStyleBase =
@ -211,8 +212,6 @@ export default function RootLayout() {
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
const typographies: Partial<Record<TextStyle, FontStyle>> = {};
(
[
@ -262,7 +261,7 @@ export default function RootLayout() {
}
return (
<QueryClientProvider client={queryClient}>
<PersistQueryClientProvider>
<AuthContextProvider>
<ThemeProvider value={DefaultTheme}>
<Stack>
@ -273,6 +272,6 @@ export default function RootLayout() {
<Toast/>
</ThemeProvider>
</AuthContextProvider>
</QueryClientProvider>
</PersistQueryClientProvider>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,20 @@
import * as React from "react"
import Svg, { SvgProps, Path } from "react-native-svg"
const CheckmarkIcon = (props: SvgProps) => (
<Svg
width={13}
height={10}
viewBox="0 0 13 10"
fill={props.color || "white"}
{...props}
>
<Path
stroke="#fff"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.95}
d="m1.48 5.489 3.2 3.178 7.2-7.15"
/>
</Svg>
)
export default CheckmarkIcon

View File

@ -0,0 +1,19 @@
import * as React from "react"
import Svg, { SvgProps, Path } from "react-native-svg"
const DrawerIcon = (props: SvgProps) => (
<Svg
width={27}
height={18}
fill="none"
{...props}
>
<Path
stroke="#83807F"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.7}
d="M2 1.995h22.667M2 9.14h14.167M2 16.285h7.083"
/>
</Svg>
)
export default DrawerIcon

View File

@ -0,0 +1,17 @@
import * as React from "react";
import Svg, { SvgProps, Path } from "react-native-svg";
const MenuDotsIcon = (props: SvgProps) => (
<Svg
width={props.width || 4}
height={props.height || 15}
viewBox="0 0 4 15"
fill="none"
{...props}
>
<Path
fill={props.color || "#7C7C7C"}
d="M.88 7.563a1.56 1.56 0 1 0 3.12 0 1.56 1.56 0 0 0-3.12 0Zm0-5.2A1.56 1.56 0 1 0 4 2.426a1.56 1.56 0 0 0-3.12-.063Zm0 10.4A1.56 1.56 0 1 0 4 12.701a1.56 1.56 0 0 0-3.12.062Z"
/>
</Svg>
);
export default MenuDotsIcon;

View File

@ -0,0 +1,20 @@
import * as React from "react"
import Svg, { SvgProps, Path } from "react-native-svg"
const TodoRepeatIcon = (props: SvgProps) => (
<Svg
width={props.width || 30}
height={props.height || 30}
viewBox="-1 -3 19 19"
fill="none"
{...props}
>
<Path
stroke={props.color || "#858585"}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.455}
d="M1.158 7.197a5.42 5.42 0 0 1 9.58-4.103m0 0V1.099m0 1.995v.037H8.705m3.21 2.71a5.42 5.42 0 0 1-9.444 4.263m0 .001v-.198h2.033m-2.033.198v1.835"
/>
</Svg>
)
export default TodoRepeatIcon

View File

@ -1,8 +1,17 @@
module.exports = function (api) {
const env = process.env.NODE_ENV;
api.cache(true);
let plugins = [];
if (env !== 'development') {
plugins.push('transform-remove-console');
}
return {
presets: [
'babel-preset-expo'
]
'babel-preset-expo',
],
plugins
};
};
};

View File

@ -1,55 +1,64 @@
export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) {
const response = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/primary/events?single_events=true&time_min=${startDate}&time_max=${endDate}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
const googleEvents = [];
let pageToken = null;
do {
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
url.searchParams.set('singleEvents', 'true');
url.searchParams.set('timeMin', startDate);
url.searchParams.set('timeMax', endDate);
if (pageToken) url.searchParams.set('pageToken', pageToken);
const data = await response.json();
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
});
const googleEvents = [];
data.items?.forEach((item) => {
let isAllDay = false;
const start = item.start;
let startDateTime;
if (start !== undefined) {
if (start.dateTime) {
const stringDate = start.dateTime;
startDateTime = new Date(stringDate);
} else {
const stringDate = start.date;
startDateTime = new Date(stringDate);
isAllDay = true;
}
}
const data = await response.json();
const end = item.end;
let endDateTime;
if (end !== undefined) {
if (end.dateTime) {
const stringDate = end.dateTime;
endDateTime = new Date(stringDate);
} else {
const stringDate = end.date;
endDateTime = new Date(stringDate);
}
}
if (!response.ok) {
throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
}
const googleEvent = {
id: item.id,
title: item.summary ?? "",
startDate: startDateTime,
endDate: endDateTime,
allDay: isAllDay,
familyId,
email
};
googleEvents.push(googleEvent);
});
data.items?.forEach((item) => {
let isAllDay = false;
let startDateTime, endDateTime;
return {googleEvents, success: response.ok};
}
if (item.start) {
if (item.start.dateTime) {
startDateTime = new Date(item.start.dateTime);
} else if (item.start.date) {
startDateTime = new Date(item.start.date);
isAllDay = true;
}
}
if (item.end) {
if (item.end.dateTime) {
endDateTime = new Date(item.end.dateTime);
} else if (item.end.date) {
endDateTime = new Date(item.end.date);
isAllDay = true;
}
}
const googleEvent = {
id: item.id,
title: item.summary || "",
startDate: startDateTime,
endDate: endDateTime,
allDay: isAllDay,
familyId,
email,
};
googleEvents.push(googleEvent);
});
// Prepare for the next page if it exists
pageToken = data.nextPageToken;
} while (pageToken);
return { googleEvents, success: true };
}

View File

@ -1,93 +1,84 @@
import { Text, TouchableOpacity, View } from "react-native-ui-lib";
import React, { useEffect, useState } from "react";
import { StyleSheet } from "react-native";
import { NavigationProp } from "@react-navigation/native";
import view from "react-native-ui-lib/src/components/view";
import {SegmentedControl, View} from "react-native-ui-lib";
import React, {memo, useCallback, useMemo, useRef, useEffect} from "react";
import {StyleSheet} from "react-native";
import {NavigationProp, useNavigationState} from "@react-navigation/native";
interface ViewSwitchProps {
navigation: NavigationProp<any>; // Adjust according to your navigation structure
navigation: NavigationProp<any>;
}
const ViewSwitch: React.FC<ViewSwitchProps> = ({ navigation }) => {
const [pageIndex, setPageIndex] = useState<number>(navigation.getState().index);
const ViewSwitch = memo(function ViewSwitch({navigation}: ViewSwitchProps) {
const currentIndex = useNavigationState((state) => state.index === 6 ? 1 : 0);
const isInitialMount = useRef(true);
const navigationPending = useRef(false);
useEffect(() => {
setPageIndex(navigation.getState().index);
}, [navigation.getState().index])
return (
<View
row
spread
style={{
borderRadius: 30,
backgroundColor: "#ebebeb",
alignItems: "center",
justifyContent: "center",
// iOS shadow
shadowColor: "#000",
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
// Android shadow (elevation)
elevation: 0,
}}
centerV
>
<TouchableOpacity
onPress={() => {
navigation.navigate("calendar");
}}
>
<View
centerV
centerH
height={54}
paddingH-15
style={ pageIndex == 1 || pageIndex == 0 ? styles.switchBtnActive : styles.switchBtn}
>
<Text color={pageIndex == 1 || pageIndex == 0 ? "white" : "black"} style={styles.switchTxt}>
Calendar
</Text>
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
}, []);
const handleSegmentChange = useCallback(
(index: number) => {
if (navigationPending.current) return;
navigationPending.current = true;
setTimeout(() => {
navigation.navigate(index === 0 ? "calendar" : "todos");
navigationPending.current = false;
}, 300);
},
[navigation]
);
const segments = useMemo(() => [
{label: "Calendar", segmentLabelStyle: styles.labelStyle},
{label: "To Dos", segmentLabelStyle: styles.labelStyle},
], []);
return (
<View style={styles.container}>
<SegmentedControl
segments={segments}
containerStyle={styles.segmentContainer}
style={styles.segment}
backgroundColor="#ebebeb"
inactiveColor="black"
activeColor="white"
activeBackgroundColor="#ea156c"
outlineColor="white"
outlineWidth={3}
onChangeIndex={handleSegmentChange}
initialIndex={currentIndex}
/>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
navigation.navigate("todos");
}}
>
<View
centerV
centerH
height={54}
paddingH-15
style={pageIndex == 6 ? styles.switchBtnActive : styles.switchBtn}
>
<Text color={pageIndex == 6 ? "white" : "black"} style={styles.switchTxt}>
Chores
</Text>
</View>
</TouchableOpacity>
</View>
);
};
export default ViewSwitch;
);
});
const styles = StyleSheet.create({
switchBtnActive: {
backgroundColor: "#ea156c",
borderRadius: 50,
width: 110,
},
switchBtn: {
backgroundColor: "#ebebeb",
borderRadius: 50,
width: 110,
},
switchTxt: {
fontSize: 16,
fontFamily: "Manrope_600SemiBold",
},
container: {
borderRadius: 30,
backgroundColor: "#ebebeb",
shadowColor: "#000",
shadowOffset: {width: 0, height: 0},
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
},
segmentContainer: {
height: 44,
width: 220,
},
segment: {
borderRadius: 50,
borderWidth: 0,
height: 44,
},
labelStyle: {
fontSize: 16,
fontFamily: "Manrope_600SemiBold",
},
});
export default ViewSwitch;

View File

@ -21,7 +21,9 @@ const TabletCalendarPage = () => {
return (
<TabletContainer>
<InnerCalendar />
<View flexG paddingB-25>
<InnerCalendar />
</View>
</TabletContainer>
);
};

View File

@ -1,5 +1,5 @@
import { View, Text, TouchableOpacity, Icon } from "react-native-ui-lib";
import React, { useState } from "react";
import React, {useEffect, useState} from "react";
import { useToDosContext } from "@/contexts/ToDosContext";
import {
addDays,
@ -12,9 +12,15 @@ import { AntDesign } from "@expo/vector-icons";
import { IToDo } from "@/hooks/firebase/types/todoData";
import ToDoItem from "../../todos/ToDoItem";
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
import { ScrollView } from "react-native-gesture-handler";
const groupToDosByDate = (toDos: IToDo[]) => {
let sortedTodos = toDos.sort((a, b) => a.date - b.date);
let sortedTodos = toDos.sort((a, b) => {
const dateA = a.date === null ? new Date() : a.date;
const dateB = b.date === null ? new Date() : b.date;
return dateA - dateB;
});
return sortedTodos.reduce(
(groups, toDo) => {
let dateKey;
@ -33,9 +39,15 @@ const groupToDosByDate = (toDos: IToDo[]) => {
});
};
if (toDo.date === null) {
dateKey = "No Date";
} else if (isToday(toDo.date)) {
const isOverdue = (date: Date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today;
};
if (isOverdue(toDo.date) && !toDo.done) {
dateKey = "Overdue";
} else if (toDo.date === null || isToday(toDo.date)) {
dateKey = "Today";
} else if (isTomorrow(toDo.date)) {
dateKey = "Tomorrow";
@ -45,7 +57,8 @@ const groupToDosByDate = (toDos: IToDo[]) => {
dateKey = "Next 30 Days";
subDateKey = format(toDo.date, "MMM d");
} else {
return groups;
dateKey = "Later";
subDateKey = format(toDo.date, "MMM d, yyyy");
}
if (!groups[dateKey]) {
@ -55,7 +68,7 @@ const groupToDosByDate = (toDos: IToDo[]) => {
};
}
if (dateKey === "Next 30 Days" && subDateKey) {
if ((dateKey === "Next 30 Days" || dateKey === "Later") && subDateKey) {
if (!groups[dateKey].subgroups[subDateKey]) {
groups[dateKey].subgroups[subDateKey] = [];
}
@ -77,15 +90,23 @@ const groupToDosByDate = (toDos: IToDo[]) => {
const filterToDosByUser = (toDos: IToDo[], uid: string | undefined) => {
if (!uid) return [];
return toDos.filter((todo) =>
todo.assignees?.includes(uid)
);
return toDos.filter((todo) => todo.assignees?.includes(uid));
};
const SingleUserChoreList = ({ user }: { user: UserProfile }) => {
const { toDos } = useToDosContext();
const userTodos = filterToDosByUser(toDos, user.uid);
const groupedToDos = groupToDosByDate(userTodos);
const [localTodos, setLocalTodos] = useState([]);
const [groupedToDos, setGroupedToDos] = useState([]);
useEffect(() => {
const userTodos = filterToDosByUser(toDos, user.uid);
setLocalTodos(userTodos);
}, [toDos, user]);
useEffect(() => {
const grouped = groupToDosByDate(localTodos);
setGroupedToDos(grouped);
}, [localTodos]);
const [expandedGroups, setExpandedGroups] = useState<{
[key: string]: boolean;
@ -101,14 +122,17 @@ const SingleUserChoreList = ({ user }: { user: UserProfile }) => {
};
const noDateToDos = groupedToDos["No Date"]?.items || [];
const datedToDos = Object.keys(groupedToDos).filter(
(key) => key !== "No Date"
);
const datedToDos = Object.keys(groupedToDos)
.filter((key) => key !== "No Date")
.sort((a, b) => {
const order = ["Overdue", "Today", "Tomorrow", "Next 7 Days", "Next 30 Days", "Later"];
return order.indexOf(a) - order.indexOf(b);
});
const renderTodoGroup = (dateKey: string) => {
const isExpanded = expandedGroups[dateKey] || false;
if (dateKey === "Next 30 Days") {
if (dateKey === "Next 30 Days" || dateKey === "Later") {
const subgroups = Object.entries(groupedToDos[dateKey].subgroups).sort(
([dateA], [dateB]) => {
const dateAObj = new Date(dateA);
@ -172,7 +196,12 @@ const SingleUserChoreList = ({ user }: { user: UserProfile }) => {
</View>
{sortedItems.map((item) => (
<ToDoItem key={item.id} item={item} is7Days={false} />
<ToDoItem
key={item.id}
item={item}
is7Days={false}
localTodos={localTodos}
setLocalTodos={setLocalTodos}/>
))}
</View>
);
@ -221,6 +250,8 @@ const SingleUserChoreList = ({ user }: { user: UserProfile }) => {
key={item.id}
item={item}
is7Days={dateKey === "Next 7 Days"}
localTodos={localTodos}
setLocalTodos={setLocalTodos}
/>
))}
</View>
@ -229,41 +260,48 @@ const SingleUserChoreList = ({ user }: { user: UserProfile }) => {
return (
<View
marginB-402
marginT-10
paddingH-10
backgroundColor="#f9f8f7"
style={{ minHeight: 800, borderRadius: 9.11 }}
style={{
minHeight: 600,
maxHeight: 600,
borderRadius: 9.11,
overflow: "hidden",
}}
width={355}
>
{noDateToDos.length > 0 && (
<View key="No Date">
<View row spread paddingH-19 marginB-12>
<Text
text70
style={{
fontWeight: "bold",
}}
>
Unscheduled
</Text>
<AntDesign
name={expandNoDate ? "caretdown" : "caretright"}
size={24}
color="#fd1575"
onPress={() => {
setExpandNoDate(!expandNoDate);
}}
/>
</View>
{expandNoDate &&
noDateToDos
.sort((a, b) => Number(a.done) - Number(b.done))
.map((item) => <ToDoItem key={item.id} item={item} />)}
</View>
)}
<ScrollView showsVerticalScrollIndicator={false}>
<View paddingH-10 paddingB-90>
{noDateToDos.length > 0 && (
<View key="No Date">
<View row spread paddingH-19 marginB-12>
<Text
text70
style={{
fontWeight: "bold",
}}
>
Unscheduled
</Text>
<AntDesign
name={expandNoDate ? "caretdown" : "caretright"}
size={24}
color="#fd1575"
onPress={() => {
setExpandNoDate(!expandNoDate);
}}
/>
</View>
{expandNoDate &&
noDateToDos
.sort((a, b) => Number(a.done) - Number(b.done))
.map((item) => <ToDoItem key={item.id} item={item} localTodos={localTodos} setLocalTodos={setLocalTodos} />)}
</View>
)}
{datedToDos.map(renderTodoGroup)}
{datedToDos.map(renderTodoGroup)}
</View>
</ScrollView>
</View>
);
};

View File

@ -1,85 +1,127 @@
import React, { useEffect } from "react";
import { View, Text } from "react-native-ui-lib";
import React, {useEffect, useMemo} from "react";
import {Text, View} from "react-native-ui-lib";
import * as ScreenOrientation from "expo-screen-orientation";
import TabletContainer from "../tablet_components/TabletContainer";
import ToDosPage from "../../todos/ToDosPage";
import ToDosList from "../../todos/ToDosList";
import SingleUserChoreList from "./SingleUserChoreList";
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
import { ImageBackground, StyleSheet } from "react-native";
import { colorMap } from "@/constants/colorMap";
import { ScrollView } from "react-native-gesture-handler";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import AddChore from "../../todos/AddChore";
import { useAtom } from "jotai";
import { selectedUserAtom } from "@/components/pages/calendar/atoms";
const TabletChoresPage = () => {
const { data: users } = useGetFamilyMembers();
// Function to lock the screen orientation to landscape
const lockScreenOrientation = async () => {
await ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
);
};
const {data: users} = useGetFamilyMembers();
const { user: currentUser } = useAuthContext();
const [selectedUser] = useAtom(selectedUserAtom);
useEffect(() => {
lockScreenOrientation(); // Lock orientation when the component mounts
const sortedUsers = useMemo(() => {
return users
?.filter(member => member.userType !== ProfileType.FAMILY_DEVICE)
.sort((a, b) => {
if (a.uid === currentUser?.uid) return -1;
if (b.uid === currentUser?.uid) return 1;
return () => {
// Optional: Unlock to default when the component unmounts
ScreenOrientation.unlockAsync();
const typePriority = {
[ProfileType.PARENT]: 0,
[ProfileType.CHILD]: 1,
[ProfileType.CAREGIVER]: 2
};
return typePriority[a.userType] - typePriority[b.userType];
});
}, [users, currentUser]);
const lockScreenOrientation = async () => {
await ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
);
};
}, []);
return (
<TabletContainer>
<ScrollView horizontal>
<View row gap-25 padding-25>
{users?.map((user, index) => (
<View>
<View row centerV>
{user.pfp ? (
<ImageBackground
source={{ uri: user.pfp }}
style={styles.pfp}
borderRadius={13.33}
/>
) : (
<View
center
style={styles.pfp}
backgroundColor={user.eventColor || "#00a8b6"}
>
<Text color="white">
{user.firstName.at(0)}
{user.lastName.at(0)}
</Text>
</View>
)}
<Text style={styles.name} marginL-15>
{user.firstName}
</Text>
<Text style={[styles.name, { color: "#9b9b9b" }]} marginL-5>
({user.userType})
</Text>
</View>
<SingleUserChoreList user={user} />
useEffect(() => {
lockScreenOrientation();
return () => {
ScreenOrientation.unlockAsync();
};
}, []);
const capitalizeFirstLetter = (str: string) => {
if (!str) return "";
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
};
return (
<TabletContainer>
<ScrollView horizontal>
<View row gap-25 padding-25>
{sortedUsers
?.filter((member) =>
!selectedUser ||
selectedUser.uid === 'family-view' ||
selectedUser.uid === member.uid
)
.map((user, index) => (
<View key={index}>
<View row centerV>
{user.pfp ? (
<ImageBackground
source={{ uri: user.pfp }}
style={styles.pfp}
imageStyle={(user.eventColor && {
borderWidth: 2,
borderColor: user.eventColor,
}) || undefined}
borderRadius={13.33}
/>
) : (
<View
center
style={styles.pfp}
backgroundColor={user.eventColor || "#00a8b6"}
>
<Text color="white">
{user.firstName.at(0)}
{user.lastName.at(0)}
</Text>
</View>
)}
<Text style={styles.name} marginL-15>
{user.firstName}
</Text>
<Text style={[styles.name, { color: "#9b9b9b" }]} marginL-5>
({capitalizeFirstLetter(user.userType)})
</Text>
</View>
<SingleUserChoreList user={user} />
</View>
))}
</View>
</ScrollView>
<View style={styles.addBtn}>
<AddChore />
</View>
))}
</View>
</ScrollView>
</TabletContainer>
);
</TabletContainer>
);
};
const styles = StyleSheet.create({
pfp: {
width: 46.74,
aspectRatio: 1,
borderRadius: 13.33,
},
name: {
fontFamily: "Manrope_600SemiBold",
fontSize: 22.43,
color: "#2c2c2c",
},
pfp: {
width: 46.74,
aspectRatio: 1,
borderRadius: 13.33,
},
name: {
fontFamily: "Manrope_600SemiBold",
fontSize: 22.43,
color: "#2c2c2c",
},
addBtn: {
position: 'absolute',
bottom: 50,
right: 220
}
});
export default TabletChoresPage;
export default TabletChoresPage;

View File

@ -1,6 +1,6 @@
import { View, Text, ViewProps } from "react-native-ui-lib";
import React, { ReactNode } from "react";
import { Dimensions, StyleSheet } from "react-native";
import React, { ReactNode, useEffect, useState } from "react";
import { Dimensions, StyleSheet, useWindowDimensions } from "react-native";
import UsersList from "./UsersList";
import { ScrollView } from "react-native-gesture-handler";
@ -8,12 +8,52 @@ interface TabletContainerProps extends ViewProps {
children: ReactNode;
}
const { width, height } = Dimensions.get("window");
const TabletContainer: React.FC<TabletContainerProps> = ({
children,
...props
}) => {
const window = useWindowDimensions();
const [containerWidth, setContainerWidth] = useState(Dimensions.get('window').width);
const [containerHeight, setContainerHeight] = useState(Dimensions.get('window').height);
// Update dimensions on mount and when window size changes
useEffect(() => {
const updateDimensions = () => {
setContainerWidth(window.width);
setContainerHeight(window.height);
};
updateDimensions();
// Force a second update after a brief delay to handle any initial rendering issues
const timer = setTimeout(updateDimensions, 100);
return () => clearTimeout(timer);
}, [window.width, window.height]);
const styles = StyleSheet.create({
container: {
backgroundColor: "white",
flex: 1,
flexDirection: 'row',
borderTopColor: "#a9a9a9",
width: containerWidth,
borderTopWidth: 1,
},
calendarContainer: {
backgroundColor: "white",
height: containerHeight,
width: containerWidth * 0.89,
},
profilesContainer: {
width: containerWidth * 0.11,
height: containerHeight,
borderLeftWidth: 1,
borderLeftColor: "#a9a9a9",
backgroundColor: "white",
},
});
return (
<View style={styles.container}>
<View row>
@ -28,25 +68,4 @@ const TabletContainer: React.FC<TabletContainerProps> = ({
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: "white",
flex: 1,
borderTopColor: "#a9a9a9",
borderTopWidth: 1,
},
calendarContainer: {
backgroundColor: "white",
height: height,
width: width * 0.89,
},
profilesContainer: {
width: width * 0.11,
height: height,
borderLeftWidth: 1,
borderLeftColor: "#a9a9a9",
backgroundColor: "white",
},
});
export default TabletContainer;
export default TabletContainer;

View File

@ -1,27 +1,86 @@
import { View, Text } from "react-native-ui-lib";
import React from "react";
import { View, Text, TouchableOpacity } from "react-native-ui-lib";
import React, { useEffect, useMemo } from "react";
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
import { ImageBackground, StyleSheet } from "react-native";
import { colorMap } from "@/constants/colorMap";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { useAtom } from "jotai";
import { selectedUserAtom } from "@/components/pages/calendar/atoms";
type UserProfile = {
uid: string;
firstName: string;
lastName: string;
userType: string;
eventColor: string;
pfp?: string;
};
const UsersList = () => {
const { data: familyMembers } = useGetFamilyMembers();
const { user: currentUser } = useAuthContext();
const { data: familyMembers, refetch: refetchFamilyMembers } =
useGetFamilyMembers();
const [selectedUser, setSelectedUser] = useAtom(selectedUserAtom);
useEffect(() => {
refetchFamilyMembers();
}, []);
const sortedMembers = useMemo(() => {
const filtered = familyMembers?.filter(
(member) => member.userType !== ProfileType.FAMILY_DEVICE
) || [];
const currentUserData = filtered.find(m => m.uid === currentUser?.uid);
const parents = filtered.filter(m => m.userType === ProfileType.PARENT && m.uid !== currentUser?.uid);
const children = filtered.filter(m => m.userType === ProfileType.CHILD && m.uid !== currentUser?.uid);
const caregivers = filtered.filter(m => m.userType === ProfileType.CAREGIVER && m.uid !== currentUser?.uid);
const familyViewOption: UserProfile = {
uid: 'family-view',
firstName: 'Family',
lastName: 'View',
userType: 'Family View',
eventColor: colorMap.pink
};
return currentUserData
? [currentUserData, ...parents, ...children, familyViewOption, ...caregivers]
: [...parents, ...children, familyViewOption, ...caregivers];
}, [familyMembers, currentUser]);
const capitalizeFirstLetter = (str: string) => {
if (!str) return '';
if (!str) return "";
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
};
return (
<View centerH paddingT-10>
{familyMembers?.map((member, index) => (
<>
<View centerH paddingT-10 marginB-70>
{sortedMembers?.map((member, index) => (
<TouchableOpacity
key={member.uid}
onPress={() => {
if (member.uid === 'family-view') {
setSelectedUser(null);
} else {
setSelectedUser(selectedUser?.uid === member.uid ? null : member);
}
}}
style={[
styles.memberContainer,
selectedUser?.uid === member.uid && styles.selectedMember,
]}
>
{member.pfp ? (
<ImageBackground
key={index}
source={{ uri: member.pfp }}
style={styles.pfp}
borderRadius={100}
imageStyle={{
borderRadius: 200,
borderWidth: 2,
borderColor: member.eventColor || colorMap.teal
}}
/>
) : (
<View
@ -29,17 +88,18 @@ const UsersList = () => {
style={styles.pfp}
center
backgroundColor={member.eventColor || colorMap.teal}
children={
<Text color="white">
{member.firstName.at(0)}
{member.lastName.at(0)}
</Text>
}
/>
>
<Text color="white">
{member.firstName.charAt(0)}
{member.lastName.charAt(0)}
</Text>
</View>
)}
<Text style={styles.fName}>{member.firstName}</Text>
<Text style={styles.role}>{capitalizeFirstLetter(member.userType)}</Text>
</>
<Text style={styles.role}>
{capitalizeFirstLetter(member.userType)}
</Text>
</TouchableOpacity>
))}
</View>
);
@ -62,6 +122,13 @@ const styles = StyleSheet.create({
color: "#9b9b9b",
marginBottom: 20,
},
memberContainer: {
alignItems: "center",
marginBottom: 20,
},
selectedMember: {
opacity: 1,
},
});
export default UsersList;
export default UsersList;

View File

@ -13,8 +13,8 @@ interface IAddBrainDumpProps {
}
const AddBrainDump = ({
addBrainDumpProps,
}: {
addBrainDumpProps,
}: {
addBrainDumpProps: IAddBrainDumpProps;
}) => {
const {addBrainDump} = useBrainDumpContext();
@ -22,11 +22,11 @@ const AddBrainDump = ({
const [dumpDesc, setDumpDesc] = useState<string>("");
const {width} = Dimensions.get("screen");
// Refs for the two TextFields
const descriptionRef = useRef<TextFieldRef>(null);
const titleRef = useRef<TextFieldRef>(null);
const isTitleValid = dumpTitle.trim().length >= 3;
useEffect(() => {
setDumpDesc("");
setDumpTitle("");
@ -40,9 +40,9 @@ const AddBrainDump = ({
}
}, [addBrainDumpProps.isVisible]);
useEffect(() => {
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
}, []);
useEffect(() => {
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
}, []);
return (
<Dialog
@ -69,18 +69,17 @@ const AddBrainDump = ({
<Button
color="#05a8b6"
label="Save"
style={styles.topBtn}
style={[styles.topBtn, !isTitleValid && styles.disabledBtn]}
disabled={!isTitleValid}
onPress={() => {
addBrainDump({
id: 99,
title: dumpTitle.trimEnd().trimStart(),
description: dumpDesc.trimEnd().trimStart(),
});
addBrainDumpProps.setIsVisible(false);
if (isTitleValid) {
addBrainDump({
id: '99',
title: dumpTitle.trim(),
description: dumpDesc.trim(),
});
addBrainDumpProps.setIsVisible(false);
}
}}
/>
</View>
@ -94,11 +93,10 @@ const AddBrainDump = ({
setDumpTitle(text);
}}
onSubmitEditing={() => {
// Move focus to the description field
descriptionRef.current?.focus();
}}
style={styles.title}
blurOnSubmit={false} // Keep the keyboard open when moving focus
blurOnSubmit={false}
returnKeyType="next"
/>
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20/>
@ -125,28 +123,31 @@ const AddBrainDump = ({
};
const styles = StyleSheet.create({
dialogContainer: {
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
backgroundColor: "white",
padding: 0,
paddingTop: 3,
margin: 0,
},
topBtns: {},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
title: {
fontSize: 22,
fontFamily: "Manrope_500Medium",
},
description: {
fontFamily: "Manrope_400Regular",
fontSize: 14,
textAlignVertical: "top",
},
dialogContainer: {
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
backgroundColor: "white",
padding: 0,
paddingTop: 3,
margin: 0,
},
topBtns: {},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
disabledBtn: {
opacity: 0.2,
},
title: {
fontSize: 22,
fontFamily: "Manrope_500Medium",
},
description: {
fontFamily: "Manrope_400Regular",
fontSize: 14,
textAlignVertical: "top",
},
});
export default AddBrainDump;
export default AddBrainDump;

View File

@ -3,14 +3,17 @@ import React, {useState} from "react";
import {Button, Text, TextField, View} from "react-native-ui-lib";
import DumpList from "./DumpList";
import HeaderTemplate from "@/components/shared/HeaderTemplate";
import {Feather, MaterialIcons} from "@expo/vector-icons";
import {Feather} from "@expo/vector-icons";
import AddBrainDump from "./AddBrainDump";
import LinearGradient from "react-native-linear-gradient";
import PlusIcon from "@/assets/svgs/PlusIcon";
import * as Device from 'expo-device'
import {DeviceType} from 'expo-device'
const BrainDumpPage = () => {
const [searchText, setSearchText] = useState<string>("");
const [isAddVisible, setIsAddVisible] = useState<boolean>(false);
const isTablet: boolean = Device.deviceType === DeviceType.TABLET;
return (
<View height={"100%"}>
@ -19,7 +22,7 @@ const BrainDumpPage = () => {
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
>
<View marginH-25>
<View marginH-25 marginT-20 style={isTablet ? {alignItems: 'center'} : undefined}>
<HeaderTemplate
message={"Welcome to your notes!"}
isWelcome={false}
@ -31,7 +34,7 @@ const BrainDumpPage = () => {
</Text>
}
/>
<View>
<View style={isTablet ? {maxWidth: 390} : undefined}>
<View style={styles.searchField} centerV>
<TextField
value={searchText}
@ -78,6 +81,7 @@ const BrainDumpPage = () => {
bottom: -10,
borderRadius: 30,
backgroundColor: "#fd1775",
maxWidth: 450,
}}
color="white"
enableShadow
@ -114,6 +118,8 @@ const styles = StyleSheet.create({
height: 42,
paddingLeft: 10,
marginVertical: 20,
marginTop: 30,
boxShadow: 'inset 0px 0px 37px 0px rgba(239,238,237,1)',
},
});

View File

@ -18,7 +18,7 @@ const BrainDumpItem = (props: { item: IBrainDump }) => {
marginV-5
paddingH-13
paddingV-10
style={{ borderRadius: 15, elevation: 2 }}
style={{ borderRadius: 15, elevation: 0 }}
>
<Text
text70B

View File

@ -1,14 +1,13 @@
import { View } from "react-native-ui-lib";
import { View, Text } from "react-native-ui-lib";
import React from "react";
import { useBrainDumpContext } from "@/contexts/DumpContext";
import { FlatList } from "react-native";
import BrainDumpItem from "./DumpItem";
import LinearGradient from "react-native-linear-gradient";
import { StyleSheet } from "react-native";
const DumpList = (props: { searchText: string }) => {
const { brainDumps } = useBrainDumpContext();
const filteredBrainDumps =
const sortedDumps =
props.searchText.trim() === ""
? brainDumps
: brainDumps.filter(
@ -19,18 +18,17 @@ const DumpList = (props: { searchText: string }) => {
.includes(props.searchText.toLowerCase())
);
return (
<View marginB-70>
<FlatList
style={{ zIndex: -1 }}
data={filteredBrainDumps}
keyExtractor={(item) => item.title}
renderItem={({ item }) => (
<BrainDumpItem key={item.title} item={item} />
)}
/>
</View>
);
};
return (
<View marginB-70>
{sortedDumps?.length ? (
sortedDumps.map((item) => (
<BrainDumpItem key={item.id} item={item} />
))) : <Text marginT-20 center style={styles.alert}>You have no notes</Text>}
</View>
);
};
const styles = StyleSheet.create({
alert: {fontFamily: "PlusJakartaSans_300Light", fontSize: 20}
})
export default DumpList;

View File

@ -145,7 +145,7 @@ const MoveBrainDump = (props: {
style={styles.optionsIcon}
/>
<Text style={styles.optionsReg}>Move to</Text>
<Text style={styles.optionsBold}> my to do's</Text>
<Text style={styles.optionsBold}> my to dos</Text>
</View>
</TouchableOpacity>
<TouchableOpacity>

View File

@ -1,5 +1,4 @@
import React, {useState} from "react";
import {MaterialIcons,} from "@expo/vector-icons";
import {Button, Card, Dialog, PanningProvider, Text, View,} from "react-native-ui-lib";
import {StyleSheet, TouchableOpacity} from "react-native";
import AddChoreDialog from "../todos/AddChoreDialog";
@ -11,6 +10,7 @@ import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import {useSetAtom} from "jotai";
import {selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
import PlusIcon from "@/assets/svgs/PlusIcon";
import {addMinutes, roundToNearestMinutes} from "date-fns";
export const AddEventDialog = () => {
const [show, setShow] = useState(false);
@ -21,7 +21,8 @@ export const AddEventDialog = () => {
const handleOpenManualInputModal = () => {
setShow(false);
setTimeout(() => {
setSelectedNewEndDate(new Date());
const roundedDate = roundToNearestMinutes(new Date(), {nearestTo: 5});
setSelectedNewEndDate(roundedDate);
}, 500);
};
@ -51,7 +52,7 @@ export const AddEventDialog = () => {
onPress={() => setShow(true)}
>
<View row centerV centerH>
<PlusIcon />
<PlusIcon/>
<Text white style={{fontSize: 16, fontFamily: 'Manrope_600SemiBold', marginLeft: 5}}>
New
</Text>
@ -82,7 +83,7 @@ export const AddEventDialog = () => {
paddingVertical: 13,
opacity: 0.5
}}
label="Scan Image"
label="Upload Image"
labelStyle={styles.btnLabel}
onPress={handleScanImageDialog}
iconSource={() => (
@ -102,26 +103,24 @@ export const AddEventDialog = () => {
labelStyle={styles.btnLabel}
onPress={handleOpenManualInputModal}
iconSource={() => (
<CalendarIcon color={"white"} style={styles.btnIcon}/>
<CalendarIcon color="white" style={styles.btnIcon}/>
)}
/>
<Button
disabled
style={{
marginBottom: 10,
// backgroundColor: "#05a8b6",
backgroundColor: "#05a8b6",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
opacity: 0.5
paddingVertical: 13
}}
label="Add To Do"
labelStyle={styles.btnLabel}
onPress={() => setChoreDialogVisible(true)}
iconSource={() => (
<NavToDosIcon
color={"white"}
color="white"
width={23}
style={styles.btnIcon}
/>

View File

@ -0,0 +1,27 @@
import React from 'react';
import {useAtomValue} from 'jotai';
import {selectedDateAtom} from '@/components/pages/calendar/atoms';
import {FlashList} from "@shopify/flash-list";
import {useDidUpdate} from "react-native-ui-lib/src/hooks";
interface CalendarControllerProps {
scrollViewRef: React.RefObject<FlashList<any>>;
centerMonthIndex: number;
}
export const CalendarController: React.FC<CalendarControllerProps> = (
{
scrollViewRef,
centerMonthIndex
}) => {
const selectedDate = useAtomValue(selectedDateAtom);
useDidUpdate(() => {
scrollViewRef.current?.scrollToIndex({
index: centerMonthIndex,
animated: false
})
}, [selectedDate, centerMonthIndex]);
return null;
};

View File

@ -1,130 +1,181 @@
import React, { memo } from "react";
import {
Button,
Picker,
PickerModes,
SegmentedControl,
Text,
View,
} from "react-native-ui-lib";
import { MaterialIcons } from "@expo/vector-icons";
import { modeMap, months } from "./constants";
import { StyleSheet } from "react-native";
import { useAtom } from "jotai";
import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms";
import { format, isSameDay } from "date-fns";
import { useAuthContext } from "@/contexts/AuthContext";
import React, {memo, useCallback, useMemo, useState} from "react";
import {StyleSheet} from "react-native";
import {Button, Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib";
import {MaterialIcons} from "@expo/vector-icons";
import {useAtom} from "jotai";
import {format} from "date-fns";
import * as Device from "expo-device";
import {useIsFetching} from "@tanstack/react-query";
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
import {months} from "./constants";
import RefreshButton from "@/components/shared/RefreshButton";
import {useCalSync} from "@/hooks/useCalSync";
type ViewMode = "day" | "week" | "month" | "3days";
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
const SEGMENTS = isTablet
? [{label: "D"}, {label: "W"}, {label: "M"}]
: [{label: "D"}, {label: "3D"}, {label: "M"}];
const MODE_MAP = {
tablet: ["day", "week", "month"],
mobile: ["day", "3days", "month"]
} as const;
export const CalendarHeader = memo(() => {
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
const [mode, setMode] = useAtom(modeAtom);
const { profileData } = useAuthContext();
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
const [mode, setMode] = useAtom(modeAtom);
const [tempIndex, setTempIndex] = useState<number | null>(null);
const handleSegmentChange = (index: number) => {
const selectedMode = modeMap.get(index);
if (selectedMode) {
setTimeout(() => {
setMode(selectedMode as "day" | "week" | "month");
}, 150);
}
};
const {resyncAllCalendars, isSyncing} = useCalSync();
const isFetching = useIsFetching({queryKey: ['events']}) > 0;
const handleMonthChange = (month: string) => {
const currentDay = selectedDate.getDate();
const currentYear = selectedDate.getFullYear();
const newMonthIndex = months.indexOf(month);
const isLoading = useMemo(() => isSyncing || isFetching, [isSyncing, isFetching]);
const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
setSelectedDate(updatedDate);
};
const handleSegmentChange = useCallback((index: number) => {
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
const selectedMode = modes[index] as ViewMode;
const isSelectedDateToday = isSameDay(selectedDate, new Date());
setTempIndex(index);
setTimeout(() => {
setMode(selectedMode);
setTempIndex(null);
}, 150);
}, [setMode]);
return (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 20,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
backgroundColor: "white",
marginBottom: 10,
}}
>
<View row centerV gap-3>
<Text style={{ fontFamily: "Manrope_500Medium", fontSize: 17 }}>
{selectedDate.getFullYear()}
</Text>
<Picker
value={months[selectedDate.getMonth()]}
placeholder={"Select Month"}
style={{ fontFamily: "Manrope_500Medium", fontSize: 17, width: 85 }}
mode={PickerModes.SINGLE}
onChange={(itemValue) => handleMonthChange(itemValue as string)}
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"} />}
topBarProps={{
title: selectedDate.getFullYear().toString(),
titleStyle: { fontFamily: "Manrope_500Medium", fontSize: 17 },
}}
>
{months.map((month) => (
<Picker.Item key={month} label={month} value={month} />
))}
</Picker>
</View>
const handleMonthChange = useCallback((month: string) => {
const newMonthIndex = months.indexOf(month);
const updatedDate = new Date(
selectedDate.getFullYear(),
newMonthIndex,
selectedDate.getDate()
);
setSelectedDate(updatedDate);
}, [selectedDate, setSelectedDate]);
<View row centerV>
{!isSelectedDateToday && (
<Button
size={"xSmall"}
marginR-0
avoidInnerPadding
style={{
borderRadius: 50,
backgroundColor: "white",
borderWidth: 0.7,
borderColor: "#dadce0",
height: 30,
paddingHorizontal: 10,
}}
labelStyle={{
fontSize: 12,
color: "black",
fontFamily: "Manrope_500Medium",
}}
label={format(new Date(), "dd/MM/yyyy")}
onPress={() => {
setSelectedDate(new Date());
}}
/>
)}
const handleRefresh = useCallback(async () => {
try {
await resyncAllCalendars();
} catch (error) {
console.error("Refresh failed:", error);
}
}, [resyncAllCalendars]);
<View>
<SegmentedControl
segments={[{ label: "D" }, { label: "W" }, { label: "M" }]}
backgroundColor="#ececec"
inactiveColor="#919191"
activeBackgroundColor="#ea156c"
activeColor="white"
outlineColor="white"
outlineWidth={3}
segmentLabelStyle={styles.segmentslblStyle}
onChangeIndex={handleSegmentChange}
initialIndex={mode === "day" ? 0 : mode === "week" ? 1 : 2}
/>
const getInitialIndex = useCallback(() => {
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
//@ts-ignore
return modes.indexOf(mode);
}, [mode]);
const renderMonthPicker = () => (
<>
{isTablet && <View flexG/>}
<View row centerV gap-1 flexS>
{isTablet && (
<Text style={styles.yearText}>
{selectedDate.getFullYear()}
</Text>
)}
<Picker
value={months[selectedDate.getMonth()]}
placeholder="Select Month"
style={styles.monthPicker}
mode={PickerModes.SINGLE}
onChange={value => handleMonthChange(value as string)}
trailingAccessory={<MaterialIcons name="keyboard-arrow-down"/>}
topBarProps={{
title: selectedDate.getFullYear().toString(),
titleStyle: styles.yearText,
}}
>
{months.map(month => (
<Picker.Item key={month} label={month} value={month}/>
))}
</Picker>
</View>
</>
);
return (
<View style={styles.container} flexG centerV>
{mode !== "month" ? renderMonthPicker() : <View flexG/>}
<View row centerV flexS>
<Button
size="xSmall"
marginR-1
avoidInnerPadding
style={styles.todayButton}
onPress={() => setSelectedDate(new Date())}
>
<MaterialIcons name="calendar-today" size={30} color="#5f6368"/>
<Text style={styles.todayDate}>{format(new Date(), "d")}</Text>
</Button>
<View style={styles.segmentContainer}>
<SegmentedControl
segments={SEGMENTS}
backgroundColor="#ececec"
inactiveColor="#919191"
activeBackgroundColor="#ea156c"
activeColor="white"
outlineColor="white"
outlineWidth={3}
segmentLabelStyle={styles.segmentLabel}
onChangeIndex={handleSegmentChange}
initialIndex={tempIndex ?? getInitialIndex()}
/>
</View>
<RefreshButton onRefresh={handleRefresh} isSyncing={isLoading}/>
</View>
</View>
</View>
</View>
);
);
});
const styles = StyleSheet.create({
segmentslblStyle: {
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},
});
container: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: isTablet ? 8 : 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
paddingLeft: 10
},
yearText: {
fontFamily: "Manrope_500Medium",
fontSize: 17,
},
monthPicker: {
fontFamily: "Manrope_500Medium",
fontSize: 17,
width: 85,
},
segmentContainer: {
maxWidth: 120,
height: 40,
},
segmentLabel: {
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},
todayButton: {
backgroundColor: "transparent",
borderWidth: 0,
height: 30,
width: 30,
alignItems: 'center',
justifyContent: 'center',
},
todayDate: {
position: 'absolute',
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
color: "#5f6368",
top: '30%',
},
});

View File

@ -1,21 +1,15 @@
import React from "react";
import {View,} from "react-native-ui-lib";
import HeaderTemplate from "@/components/shared/HeaderTemplate";
import {InnerCalendar} from "@/components/pages/calendar/InnerCalendar";
import { View } from "react-native-ui-lib";
import { InnerCalendar } from "@/components/pages/calendar/InnerCalendar";
export default function CalendarPage() {
return (
<View
style={{flex: 1, height: "100%", padding: 10}}
paddingH-22
paddingT-0
>
<HeaderTemplate
message={"Let's get your week started!"}
isWelcome
isCalendar={true}
/>
<InnerCalendar/>
</View>
);
return (
<View
style={{ flex: 1, height: "100%", padding: 0 }}
paddingH-0
paddingT-0
>
<InnerCalendar />
</View>
);
}

View File

@ -1,92 +1,94 @@
import { Text, TouchableOpacity, View } from "react-native-ui-lib";
import React from "react";
import { StyleSheet } from "react-native";
import { useAtom } from "jotai";
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
import {Text, TouchableOpacity, View} from "react-native-ui-lib";
import React, {useState, useCallback} from "react";
import {StyleSheet} from "react-native";
import {useAtom} from "jotai";
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
const CalendarViewSwitch = () => {
const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom);
const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom);
const [localState, setLocalState] = useState(isFamilyView);
return (
<View
row
spread
style={{
position: "absolute",
bottom: 20,
left: 20,
borderRadius: 30,
backgroundColor: "white",
alignItems: "center",
justifyContent: "center",
// iOS shadow
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
// Android shadow (elevation)
elevation: 6,
}}
centerV
>
<TouchableOpacity
onPress={() => {
setIsFamilyView(true);
}}
>
<View
centerV
centerH
height={40}
paddingH-15
style={isFamilyView ? styles.switchBtnActive : styles.switchBtn}
>
<Text
color={isFamilyView ? "white" : "#a1a1a1"}
style={styles.switchTxt}
>
Family View
</Text>
</View>
</TouchableOpacity>
const handleViewChange = useCallback((newValue: boolean) => {
setLocalState(newValue);
setTimeout(() => {
setIsFamilyView(newValue);
}, 150);
}, [setIsFamilyView]);
<TouchableOpacity
onPress={() => {
setIsFamilyView(false);
}}
>
return (
<View
centerV
centerH
height={40}
paddingH-15
style={!isFamilyView ? styles.switchBtnActive : styles.switchBtn}
row
spread
style={{
position: "absolute",
bottom: 20,
left: 20,
borderRadius: 30,
backgroundColor: "white",
alignItems: "center",
justifyContent: "center",
shadowColor: "#000",
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 6,
}}
centerV
>
<Text
color={!isFamilyView ? "white" : "#a1a1a1"}
style={styles.switchTxt}
>
My View
</Text>
<TouchableOpacity
onPress={() => handleViewChange(true)}
>
<View
centerV
centerH
height={40}
paddingH-15
style={localState ? styles.switchBtnActive : styles.switchBtn}
>
<Text
color={localState ? "white" : "#a1a1a1"}
style={styles.switchTxt}
>
Family View
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleViewChange(false)}
>
<View
centerV
centerH
height={40}
paddingH-15
style={!localState ? styles.switchBtnActive : styles.switchBtn}
>
<Text
color={!localState ? "white" : "#a1a1a1"}
style={styles.switchTxt}
>
My View
</Text>
</View>
</TouchableOpacity>
</View>
</TouchableOpacity>
</View>
);
);
};
export default CalendarViewSwitch;
const styles = StyleSheet.create({
switchBtnActive: {
backgroundColor: "#a1a1a1",
borderRadius: 50,
},
switchBtn: {
backgroundColor: "white",
borderRadius: 50,
},
switchTxt: {
fontSize: 16,
fontFamily: "Manrope_600SemiBold",
},
});
switchBtnActive: {
backgroundColor: "#a1a1a1",
borderRadius: 50,
},
switchBtn: {
backgroundColor: "white",
borderRadius: 50,
},
switchTxt: {
fontSize: 16,
fontFamily: "Manrope_600SemiBold",
},
});

View File

@ -0,0 +1,122 @@
import React, {useCallback, useMemo, useRef, useState} from "react";
import {View} from "react-native-ui-lib";
import {DeviceType} from "expo-device";
import * as Device from "expo-device";
import {CalendarBody, CalendarContainer, CalendarHeader, CalendarKitHandle} from "@howljs/calendar-kit";
import {useAtomValue} from "jotai";
import {selectedUserAtom} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useFormattedEvents} from "@/components/pages/calendar/useFormattedEvents";
import {useCalendarControls} from "@/components/pages/calendar/useCalendarControls";
import {EventCell} from "@/components/pages/calendar/EventCell";
import {DetailedCalendarController} from "@/components/pages/calendar/DetailedCalendarController";
interface EventCalendarProps {
calendarWidth: number;
mode: "week" | "month" | "day" | "3days";
onLoad?: () => void;
}
const HEADER_PROPS = {
dayBarHeight: 60,
headerBottomHeight: 20,
};
const BODY_PROPS = {
showNowIndicator: true,
hourFormat: "h:mm a"
};
const MODE_TO_DAYS = {
'week': 7,
'3days': 3,
'day': 1,
'month': 1
} as const;
const getContainerProps = (date: Date, customKey: string) => ({
hourWidth: 70,
allowPinchToZoom: true,
useHaptic: true,
scrollToNow: true,
initialDate: customKey !== "default" ? customKey : date.toISOString(),
});
const MemoizedEventCell = React.memo(EventCell, (prev, next) => {
return prev.event.id === next.event.id &&
prev.event.lastModified === next.event.lastModified;
});
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo(({
calendarWidth,
mode,
onLoad
}) => {
const {profileData} = useAuthContext();
const {data: familyMembers} = useGetFamilyMembers();
const {data: events} = useGetEvents();
const selectedUser = useAtomValue(selectedUserAtom);
const calendarRef = useRef<CalendarKitHandle>(null);
const [customKey, setCustomKey] = useState("defaultKey");
const memoizedFamilyMembers = useMemo(() => familyMembers || [], [familyMembers]);
const currentDate = useMemo(() => new Date(), []);
const containerProps = useMemo(() => getContainerProps(currentDate, customKey), [currentDate, customKey]);
const {data: formattedEvents} = useFormattedEvents(events ?? [], currentDate, selectedUser);
const {
handlePressEvent,
handlePressCell,
debouncedOnDateChanged
} = useCalendarControls(events ?? []);
const getAttendees = useCallback((event: any) => {
return memoizedFamilyMembers.filter(member => event?.attendees?.includes(member?.uid!));
}, [memoizedFamilyMembers]);
const renderEvent = useCallback((event: any) => {
const attendees = getAttendees(event);
return (
<MemoizedEventCell
event={event}
onPress={handlePressEvent}
attendees={attendees}
/>
);
}, [getAttendees, handlePressEvent]);
return (
<CalendarContainer
ref={calendarRef}
{...containerProps}
numberOfDays={MODE_TO_DAYS[mode]}
calendarWidth={calendarWidth}
onDateChanged={debouncedOnDateChanged}
firstDay={profileData?.firstDayOfWeek === "Mondays" ? 1 : 0}
events={formattedEvents ?? []}
onPressEvent={handlePressEvent}
onPressBackground={handlePressCell}
onLoad={onLoad}
>
<DetailedCalendarController
calendarRef={calendarRef}
setCustomKey={setCustomKey}
/>
<CalendarHeader {...HEADER_PROPS}/>
<CalendarBody
{...BODY_PROPS}
renderEvent={renderEvent}
/>
{Device.deviceType === DeviceType.TABLET && (
<View style={{backgroundColor: 'white', height: '9%', width: '100%'}}/>
)}
</CalendarContainer>
);
});
DetailedCalendar.displayName = 'DetailedCalendar';
export default DetailedCalendar;

View File

@ -0,0 +1,38 @@
import React, {useCallback, useEffect, useRef} from 'react';
import {useAtomValue} from 'jotai';
import {useAtomCallback} from 'jotai/utils';
import {modeAtom, selectedDateAtom} from '@/components/pages/calendar/atoms';
import {isToday} from 'date-fns';
import {CalendarKitHandle} from "@howljs/calendar-kit";
interface DetailedCalendarDateControllerProps {
calendarRef: React.RefObject<CalendarKitHandle>;
setCustomKey: (key: string) => void;
}
export const DetailedCalendarController: React.FC<DetailedCalendarDateControllerProps> = ({
calendarRef,
setCustomKey
}) => {
const selectedDate = useAtomValue(selectedDateAtom);
const lastSelectedDate = useRef(selectedDate);
const checkModeAndGoToDate = useAtomCallback(useCallback((get) => {
const currentMode = get(modeAtom);
if ((selectedDate && isToday(selectedDate)) || currentMode === "month") {
if (currentMode === "month") {
setCustomKey(selectedDate.toISOString());
}
calendarRef?.current?.goToDate({date: selectedDate});
}
}, [selectedDate, calendarRef, setCustomKey]));
useEffect(() => {
if (selectedDate !== lastSelectedDate.current) {
checkModeAndGoToDate();
lastSelectedDate.current = selectedDate;
}
}, [selectedDate, checkModeAndGoToDate]);
return null;
};

View File

@ -1,337 +1,111 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Calendar } from "react-native-big-calendar";
import { ActivityIndicator, StyleSheet, View, ViewStyle } from "react-native";
import { useGetEvents } from "@/hooks/firebase/useGetEvents";
import { useAtom, useSetAtom } from "jotai";
import {
editVisibleAtom,
eventForEditAtom,
isAllDayAtom,
modeAtom,
selectedDateAtom,
selectedNewEventDateAtom,
} from "@/components/pages/calendar/atoms";
import { useAuthContext } from "@/contexts/AuthContext";
import { CalendarEvent } from "@/components/pages/calendar/interfaces";
import { Text } from "react-native-ui-lib";
import { addDays, compareAsc, isWithinInterval, subDays } from "date-fns";
import React from 'react';
import {StyleSheet, View, ActivityIndicator} from 'react-native';
import {Text} from 'react-native-ui-lib';
import Animated, {
withTiming,
useAnimatedStyle,
FadeOut,
useSharedValue,
} from 'react-native-reanimated';
import {useGetEvents} from '@/hooks/firebase/useGetEvents';
import {useCalSync} from '@/hooks/useCalSync';
import {useSyncEvents} from '@/hooks/useSyncOnScroll';
import {useAtom} from 'jotai';
import {modeAtom} from './atoms';
import {MonthCalendar} from "@/components/pages/calendar/MonthCalendar";
import DetailedCalendar from "@/components/pages/calendar/DetailedCalendar";
import * as Device from "expo-device";
export type CalendarMode = 'month' | 'day' | '3days' | 'week';
interface EventCalendarProps {
calendarHeight: number;
// WAS USED FOR SCROLLABLE CALENDARS, PERFORMANCE WAS NOT OPTIMAL
calendarWidth: number;
calendarWidth: number;
}
const getTotalMinutes = () => {
const date = new Date();
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
};
export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) => {
const {isLoading} = useGetEvents();
const [mode] = useAtom<CalendarMode>(modeAtom);
const {isSyncing} = useSyncEvents();
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
const isCalendarReady = useSharedValue(false);
useCalSync();
export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
({ calendarHeight }) => {
const { data: events, isLoading } = useGetEvents();
const { profileData } = useAuthContext();
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
const [mode, setMode] = useAtom(modeAtom);
const setEditVisible = useSetAtom(editVisibleAtom);
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
const setEventForEdit = useSetAtom(eventForEditAtom);
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
const todaysDate = new Date();
const handlePressEvent = useCallback(
(event: CalendarEvent) => {
if (mode === "day" || mode === "week") {
setEditVisible(true);
console.log({ event });
setEventForEdit(event);
} else {
setMode("day");
setSelectedDate(event.start);
}
},
[setEditVisible, setEventForEdit, mode]
);
const handlePressCell = useCallback(
(date: Date) => {
if (mode === "day" || mode === "week") {
setSelectedNewEndDate(date);
} else {
setMode("day");
setSelectedDate(date);
}
},
[mode, setSelectedNewEndDate, setSelectedDate]
);
const handlePressDayHeader = useCallback(
(date: Date) => {
setIsAllDay(true);
console.log(isAllDay);
setSelectedNewEndDate(date);
setEditVisible(true);
},
[setSelectedNewEndDate]
);
const handleSwipeEnd = useCallback(
(date: Date) => {
setSelectedDate(date);
},
[setSelectedDate]
);
const memoizedEventCellStyle = useCallback(
(event: CalendarEvent) => ({ backgroundColor: event.eventColor }),
[]
);
const memoizedWeekStartsOn = useMemo(
() => (profileData?.firstDayOfWeek === "Mondays" ? 1 : 0),
[profileData]
);
console.log({
memoizedWeekStartsOn,
profileData: profileData?.firstDayOfWeek,
});
const isSameDate = useCallback((date1: Date, date2: Date) => {
return (
date1.getDate() === date2.getDate() &&
date1.getMonth() === date2.getMonth() &&
date1.getFullYear() === date2.getFullYear()
);
const handleRenderComplete = React.useCallback(() => {
isCalendarReady.value = true;
}, []);
const dayHeaderColor = useMemo(() => {
return isSameDate(todaysDate, selectedDate) ? "white" : "#4d4d4d";
}, [selectedDate, mode]);
const containerStyle = useAnimatedStyle(() => ({
opacity: withTiming(isCalendarReady.value ? 1 : 0, {duration: 500}),
flex: 1,
}));
const dateStyle = useMemo(() => {
if (mode === "week") return undefined;
return isSameDate(todaysDate, selectedDate) && mode === "day"
? styles.dayHeader
: styles.otherDayHeader;
}, [selectedDate, mode]);
const monthStyle = useAnimatedStyle(() => ({
opacity: withTiming(mode === 'month' ? 1 : 0, {duration: 300}),
position: 'absolute',
width: '100%',
height: '100%',
}));
const memoizedHeaderContentStyle = useMemo(() => {
if (mode === "day") {
return styles.dayModeHeader;
} else if (mode === "week") {
return styles.weekModeHeader;
} else if (mode === "month") {
return styles.monthModeHeader;
} else {
return {};
}
}, [mode]);
const detailedDayStyle = useAnimatedStyle(() => ({
opacity: withTiming(mode === 'day' ? 1 : 0, {duration: 300}),
position: 'absolute',
width: '100%',
height: '100%',
}));
const { enrichedEvents, filteredEvents } = useMemo(() => {
const startTime = Date.now(); // Start timer
const startOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
const endOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
const filteredEvents =
events?.filter(
(event) =>
event.start &&
event.end &&
isWithinInterval(event.start, {
start: subDays(selectedDate, startOffset),
end: addDays(selectedDate, endOffset),
}) &&
isWithinInterval(event.end, {
start: subDays(selectedDate, startOffset),
end: addDays(selectedDate, endOffset),
})
) ?? [];
const enrichedEvents = filteredEvents.reduce((acc, event) => {
const dateKey = event.start.toISOString().split("T")[0];
acc[dateKey] = acc[dateKey] || [];
acc[dateKey].push({
...event,
overlapPosition: false,
overlapCount: 0,
});
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
return acc;
}, {} as Record<string, CalendarEvent[]>);
const endTime = Date.now();
console.log(
"memoizedEvents computation time:",
endTime - startTime,
"ms"
);
return { enrichedEvents, filteredEvents };
}, [events, selectedDate, mode]);
const renderCustomDateForMonth = (date: Date) => {
const circleStyle = useMemo<ViewStyle>(
() => ({
width: 30,
height: 30,
justifyContent: "center",
alignItems: "center",
borderRadius: 15,
}),
[]
);
const defaultStyle = useMemo<ViewStyle>(
() => ({
...circleStyle,
}),
[circleStyle]
);
const currentDateStyle = useMemo<ViewStyle>(
() => ({
...circleStyle,
backgroundColor: "#4184f2",
}),
[circleStyle]
);
const renderDate = useCallback(
(date: Date) => {
const isCurrentDate = isSameDate(todaysDate, date);
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
return (
<View style={{ alignItems: "center" }}>
<View style={appliedStyle}>
<Text style={{ color: isCurrentDate ? "white" : "black" }}>
{date.getDate()}
</Text>
</View>
</View>
);
},
[todaysDate, currentDateStyle, defaultStyle] // dependencies
);
return renderDate(date);
};
useEffect(() => {
setOffsetMinutes(getTotalMinutes());
}, [events, mode]);
if (isLoading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0000ff" />
</View>
);
}
// console.log(enrichedEvents, filteredEvents)
const detailedMultiStyle = useAnimatedStyle(() => ({
opacity: withTiming(mode === (isTablet ? 'week' : '3days') ? 1 : 0, {duration: 300}),
position: 'absolute',
width: '100%',
height: '100%',
}));
return (
<Calendar
bodyContainerStyle={styles.calHeader}
swipeEnabled
mode={mode}
enableEnrichedEvents={true}
sortedMonthView
// enrichedEventsByDate={enrichedEvents}
events={filteredEvents}
// eventCellStyle={memoizedEventCellStyle}
onPressEvent={handlePressEvent}
weekStartsOn={memoizedWeekStartsOn}
height={calendarHeight}
activeDate={todaysDate}
date={selectedDate}
onPressCell={handlePressCell}
headerContentStyle={memoizedHeaderContentStyle}
onSwipeEnd={handleSwipeEnd}
scrollOffsetMinutes={offsetMinutes}
theme={{
palette: {
nowIndicator: profileData?.eventColor || "#fd1575",
gray: {
"100": "#e8eaed",
"200": "#e8eaed",
"500": "#b7b7b7",
"800": "#919191",
},
},
typography: {
fontFamily: "PlusJakartaSans_500Medium",
sm: { fontFamily: "Manrope_600SemiBold", fontSize: 15 },
xl: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
},
moreLabel: {},
xs: { fontSize: 10 },
},
}}
dayHeaderStyle={dateStyle}
dayHeaderHighlightColor={"white"}
showAdjacentMonths
hourStyle={styles.hourStyle}
onPressDateHeader={handlePressDayHeader}
ampm
// renderCustomDateForMonth={renderCustomDateForMonth}
/>
<View style={styles.root}>
{(isLoading || isSyncing) && mode !== 'month' && (
<Animated.View
exiting={FadeOut.duration(300)}
style={styles.loadingContainer}
>
{isSyncing && <Text>Syncing...</Text>}
<ActivityIndicator size="large" color="#0000ff"/>
</Animated.View>
)}
<Animated.View style={containerStyle}>
<Animated.View style={monthStyle} pointerEvents={mode === 'month' ? 'auto' : 'none'}>
<MonthCalendar/>
</Animated.View>
<Animated.View style={detailedDayStyle} pointerEvents={mode === 'day' ? 'auto' : 'none'}>
<DetailedCalendar mode="day" {...props} />
</Animated.View>
<Animated.View style={detailedMultiStyle}
pointerEvents={mode === (isTablet ? 'week' : '3days') ? 'auto' : 'none'}>
{!isLoading && (
<DetailedCalendar
onLoad={handleRenderComplete}
mode={isTablet ? 'week' : '3days'}
{...props}
/>
)}
</Animated.View>
</Animated.View>
</View>
);
}
);
});
const styles = StyleSheet.create({
segmentslblStyle: {
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},
calHeader: {
borderWidth: 0,
},
dayModeHeader: {
alignSelf: "flex-start",
justifyContent: "space-between",
alignContent: "center",
width: 38,
right: 42,
height: 13,
},
weekModeHeader: {},
monthModeHeader: {},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
dayHeader: {
backgroundColor: "#4184f2",
aspectRatio: 1,
borderRadius: 100,
alignItems: "center",
justifyContent: "center",
},
otherDayHeader: {
backgroundColor: "transparent",
color: "#919191",
aspectRatio: 1,
borderRadius: 100,
alignItems: "center",
justifyContent: "center",
},
hourStyle: {
color: "#5f6368",
fontSize: 12,
fontFamily: "Manrope_500Medium",
},
root: {
flex: 1,
},
loadingContainer: {
position: 'absolute',
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
zIndex: 100,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
},
});
export default EventCalendar;

View File

@ -0,0 +1,124 @@
import React, { useEffect, useState } from 'react';
import {StyleSheet, TouchableOpacity, View} from 'react-native';
import {Text} from 'react-native-ui-lib';
import CachedImage from 'expo-cached-image';
import {format} from 'date-fns';
import {colorMap, getEventTextColor} from '@/constants/colorMap';
interface EventCellProps {
event: any;
onPress: (event: any) => void;
attendees: any[];
}
export const EventCell: React.FC<EventCellProps> = React.memo((
{
event,
onPress,
attendees
}) => {
const [textColor, setTextColor] = useState<string>(getEventTextColor(event.color)); // Add optional chaining
useEffect(() => {
setTextColor(getEventTextColor(event?.color))
}, [event])
return (
<TouchableOpacity
onPress={() => onPress(event)}
style={[styles.eventCell, {backgroundColor: event.eventColor}]}
>
<Text style={[styles.eventTitle, {color: textColor}]} numberOfLines={1}>
{event.title}
</Text>
<Text style={[styles.eventTitle, {fontSize: 9, opacity: 0.95, color: textColor}]}>
{format(new Date(event.start.dateTime), 'h:mm a')} - {format(new Date(event.end.dateTime), 'h:mm a')}
</Text>
{attendees.length > 0 && (
<View style={styles.attendeesContainer}>
{attendees.slice(0, 3).map((attendee, index) => (
<View
key={attendee?.uid}
style={[styles.attendeeIcon, {left: index * 19}]}
>
{attendee.pfp ? (
<CachedImage
source={{uri: attendee.pfp}}
style={styles.attendeeImage}
cacheKey={attendee.pfp}
/>
) : (
<Text style={styles.attendeeInitials}>
{attendee?.firstName?.at(0)}
{attendee?.lastName?.at(0)}
</Text>
)}
</View>
))}
{attendees.length > 3 && (
<View style={[styles.attendeeCount, {left: 3 * 19}]}>
<Text style={styles.attendeeCountText}>
+{attendees.length - 3}
</Text>
</View>
)}
</View>
)}
</TouchableOpacity>
);
});
const styles = StyleSheet.create({
eventCell: {
flex: 1,
borderRadius: 5,
paddingHorizontal: 8,
paddingBottom: 8,
height: '100%',
},
eventTitle: {
fontSize: 12,
fontFamily: "PlusJakartaSans_500Medium",
},
attendeesContainer: {
flexDirection: 'row',
marginTop: 8,
height: 27.32,
},
attendeeIcon: {
position: 'absolute',
width: 20,
height: 20,
borderRadius: 50,
borderWidth: 2,
borderColor: '#f2f2f2',
overflow: 'hidden',
},
attendeeImage: {
width: '100%',
height: '100%',
},
attendeeInitials: {
color: 'white',
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},
attendeeCount: {
position: 'absolute',
width: 27.32,
height: 27.32,
borderRadius: 50,
borderWidth: 2,
borderColor: '#f2f2f2',
backgroundColor: colorMap.pink,
justifyContent: 'center',
alignItems: 'center',
},
attendeeCountText: {
color: 'white',
fontFamily: "Manrope_600SemiBold",
fontSize: 12,
},
});

View File

@ -1,37 +1,35 @@
import {View} from "react-native-ui-lib";
import React, {useRef, useState} from "react";
import React, {useCallback, useRef, useState} from "react";
import {LayoutChangeEvent} from "react-native";
import CalendarViewSwitch from "@/components/pages/calendar/CalendarViewSwitch";
import {AddEventDialog} from "@/components/pages/calendar/AddEventDialog";
import {ManuallyAddEventModal} from "@/components/pages/calendar/ManuallyAddEventModal";
import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader";
import {EventCalendar} from "@/components/pages/calendar/EventCalendar";
export const InnerCalendar = () => {
const [calendarHeight, setCalendarHeight] = useState(0);
const [calendarWidth, setCalendarWidth] = useState(0);
const calendarContainerRef = useRef(null);
const hasSetInitialSize = useRef(false);
const onLayout = (event: LayoutChangeEvent) => {
const {height, width} = event.nativeEvent.layout;
setCalendarHeight(height);
setCalendarWidth(width);
};
const onLayout = useCallback((event: LayoutChangeEvent) => {
if (!hasSetInitialSize.current) {
const width = event.nativeEvent.layout.width;
setCalendarWidth(width);
hasSetInitialSize.current = true;
}
}, []);
return (
<>
<View
style={{flex: 1, backgroundColor: "#fff", borderRadius: 30, marginBottom: 60, overflow: "hidden"}}
style={{flex: 1, backgroundColor: "#fff", borderRadius: 0, marginBottom: 0, overflow: "hidden"}}
ref={calendarContainerRef}
onLayout={onLayout}
paddingB-0
>
<CalendarHeader/>
{calendarHeight > 0 && (
<EventCalendar
calendarHeight={calendarHeight}
calendarWidth={calendarWidth}
/>
)}
<EventCalendar
calendarWidth={calendarWidth}
/>
</View>
<CalendarViewSwitch/>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,572 @@
import React, {useCallback, useMemo, useRef} from 'react';
import {Dimensions, StyleSheet, Text, TouchableOpacity, View,} from 'react-native';
import {
addDays,
addMonths,
eachDayOfInterval,
endOfMonth,
format,
isSameDay,
isSameMonth,
isWithinInterval,
startOfMonth,
} from 'date-fns';
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useSetAtom} from "jotai";
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
import {FlashList} from "@shopify/flash-list";
import * as Device from "expo-device";
import {CalendarController} from "@/components/pages/calendar/CalendarController";
interface CalendarEvent {
id: string;
title: string;
start: Date;
end: Date;
color?: string;
}
interface CustomMonthCalendarProps {
weekStartsOn?: 0 | 1;
}
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const MAX_VISIBLE_EVENTS = 3;
const CENTER_MONTH_INDEX = 12;
const Event = React.memo(({event, onPress}: { event: CalendarEvent; onPress: () => void }) => (
<TouchableOpacity
style={[styles.event, {backgroundColor: event?.eventColor || '#6200ee'}]}
onPress={onPress}
>
<Text style={styles.eventText} numberOfLines={1}>
{event.title}
</Text>
</TouchableOpacity>
));
interface CalendarEvent {
id: string;
title: string;
start: Date;
end: Date;
color?: string;
weekPosition?: number;
}
const MultiDayEvent = React.memo(({
event,
isStart,
isEnd,
onPress,
}: {
event: CalendarEvent;
dayWidth: number;
isStart: boolean;
isEnd: boolean;
onPress: () => void;
}) => {
const style = {
position: 'absolute' as const,
height: 14,
backgroundColor: event?.eventColor || '#6200ee',
padding: 2,
zIndex: 1,
left: isStart ? 4 : -0.5, // Extend slightly into the border
right: isEnd ? 4 : -0.5, // Extend slightly into the border
top: event.weekPosition ? event.weekPosition * 24 : 0,
borderRadius: 4,
borderTopLeftRadius: isStart ? 4 : 0,
borderBottomLeftRadius: isStart ? 4 : 0,
borderTopRightRadius: isEnd ? 4 : 0,
borderBottomRightRadius: isEnd ? 4 : 0,
justifyContent: 'center',
};
return (
<TouchableOpacity style={style} onPress={onPress}>
{isStart && (
<Text style={[styles.eventText]} numberOfLines={1}>
{event.title}
</Text>
)}
</TouchableOpacity>
);
});
const Day = React.memo((
{
date,
events,
multiDayEvents,
dayWidth,
onPress
}: {
date: Date;
events: CalendarEvent[];
multiDayEvents: CalendarEvent[];
dayWidth: number;
onPress: (date: Date) => void;
}) => {
const isCurrentMonth = isSameMonth(date, new Date());
const isToday = isSameDay(date, new Date());
const remainingSlots = Math.max(0, MAX_VISIBLE_EVENTS - multiDayEvents.length);
const singleDayEvents = events.filter(event => !event.isMultiDay);
const visibleSingleDayEvents = singleDayEvents.slice(0, remainingSlots);
const totalHiddenEvents = singleDayEvents.length - remainingSlots;
const maxMultiDayPosition = multiDayEvents.length > 0
? Math.max(...multiDayEvents.map(e => e.weekPosition || 0)) + 1
: 0;
const multiDayEventsHeight = maxMultiDayPosition * 16; // Height for multi-day events
return (
<View style={[styles.day, {width: dayWidth}]}>
<TouchableOpacity
style={styles.dayContent}
onPress={() => onPress(date)}
>
<View style={[
styles.dateContainer,
isToday && {backgroundColor: events?.[0]?.eventColor},
]}>
<Text style={[
styles.dateText,
!isCurrentMonth && styles.outsideMonthText,
isToday && styles.todayText,
]}>
{format(date, 'd')}
</Text>
</View>
{/* Multi-day events container */}
<View style={[styles.multiDayContainer, {height: multiDayEventsHeight}]}>
{multiDayEvents.map(event => (
<MultiDayEvent
key={event.id}
event={event}
dayWidth={dayWidth}
isStart={isSameDay(date, event.start)}
isEnd={isSameDay(date, event.end)}
onPress={() => onPress(event.start)}
/>
))}
</View>
{/* Single-day events container */}
<View style={[styles.singleDayContainer, {marginTop: multiDayEventsHeight}]}>
{visibleSingleDayEvents.map(event => (
<Event
key={event.id}
event={event}
onPress={() => onPress(event.start)}
/>
))}
{totalHiddenEvents > 0 && (
<Text style={styles.moreEvents}>
{totalHiddenEvents} More
</Text>
)}
</View>
</TouchableOpacity>
</View>
);
});
export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
const {data: rawEvents} = useGetEvents();
const setSelectedDate = useSetAtom(selectedDateAtom);
const setMode = useSetAtom(modeAtom);
const {profileData} = useAuthContext();
const scrollViewRef = useRef<FlashList<any>>(null);
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
const screenWidth = isTablet ? Dimensions.get('window').width * 0.89 : Dimensions.get('window').width;
const screenHeight = isTablet ? Dimensions.get('window').height * 0.89 : Dimensions.get('window').height;
const dayWidth = screenWidth / 7;
const centerMonth = useRef(new Date());
const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1;
const onDayPress = useCallback(
(date: Date) => {
date && setSelectedDate(date);
setTimeout(() => {
setMode("day");
}, 100)
},
[setSelectedDate, setMode]
);
const getMonthData = useCallback((date: Date) => {
const start = startOfMonth(date);
const end = endOfMonth(date);
const days = eachDayOfInterval({start, end});
const firstDay = days[0];
const startPadding = [];
let startDay = firstDay.getDay();
while (startDay !== weekStartsOn) {
startDay = (startDay - 1 + 7) % 7;
startPadding.unshift(addDays(firstDay, -startPadding.length - 1));
}
const lastDay = days[days.length - 1];
const endPadding = [];
let endDay = lastDay.getDay();
while (endDay !== (weekStartsOn + 6) % 7) {
endDay = (endDay + 1) % 7;
endPadding.push(addDays(lastDay, endPadding.length + 1));
}
return [...startPadding, ...days, ...endPadding];
}, [weekStartsOn]);
const monthsToRender = useMemo(() => {
const months = [];
for (let i = -CENTER_MONTH_INDEX; i <= CENTER_MONTH_INDEX; i++) {
const monthDate = addMonths(centerMonth.current, i);
months.push({
date: monthDate,
days: getMonthData(monthDate)
});
}
return months;
}, [getMonthData, rawEvents]);
const processedEvents = useMemo(() => {
if (!rawEvents?.length) return {
eventMap: new Map(),
multiDayEvents: []
};
const eventMap = new Map();
const multiDayEvents: CalendarEvent[] = [];
rawEvents.forEach((event) => {
if (!event?.start || !event?.end) return;
const startDate = event.start instanceof Date ? event.start : new Date(event.start);
const endDate = event.end instanceof Date ? event.end : new Date(event.end);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) return;
const duration = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
if (duration > 1) {
multiDayEvents.push({
...event,
isMultiDay: true,
start: startDate,
end: endDate
});
} else {
const dateStr = format(startDate, 'yyyy-MM-dd');
const existing = eventMap.get(dateStr) || [];
eventMap.set(dateStr, [...existing, {...event, start: startDate, end: endDate}]);
}
});
multiDayEvents.sort((a, b) => {
if (!a.start || !b.start || !a.end || !b.end) return 0;
const durationA = a.end.getTime() - a.start.getTime();
const durationB = b.end.getTime() - b.start.getTime();
return durationB - durationA;
});
return {eventMap, multiDayEvents};
}, [rawEvents]);
const getMultiDayEventsForDay = useCallback((date: Date) => {
return processedEvents.multiDayEvents.filter(event => {
if (!event.start || !event.end) return false;
return isWithinInterval(date, {
start: event.start,
end: event.end
});
});
}, [processedEvents.multiDayEvents]);
const getEventsForDay = useCallback((date: Date) => {
const dateStr = format(date, 'yyyy-MM-dd');
return processedEvents.eventMap.get(dateStr) || [];
}, [processedEvents.eventMap]);
const sortedDaysOfWeek = useMemo(() => {
const days = [...DAYS_OF_WEEK];
return days.slice(weekStartsOn).concat(days.slice(0, weekStartsOn));
}, [weekStartsOn]);
const renderMonth = useCallback(({item}: { item: MonthData }) => (
<Month
date={item.date}
days={item.days}
getEventsForDay={getEventsForDay}
getMultiDayEventsForDay={getMultiDayEventsForDay}
dayWidth={dayWidth}
onPress={onDayPress}
screenWidth={screenWidth}
sortedDaysOfWeek={sortedDaysOfWeek}
/>
), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]);
return (
<View style={styles.container}>
<CalendarController
scrollViewRef={scrollViewRef}
centerMonthIndex={CENTER_MONTH_INDEX}
/>
<FlashList
ref={scrollViewRef}
data={monthsToRender}
renderItem={renderMonth}
keyExtractor={keyExtractor}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
initialScrollIndex={CENTER_MONTH_INDEX}
removeClippedSubviews={true}
estimatedItemSize={screenWidth}
estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
autoscrollToTopThreshold: 10,
}}
/>
</View>
);
};
type MonthData = {
date: Date;
days: Date[];
};
const keyExtractor = (item: MonthData, index: number) => `month-${index}`;
const Month = React.memo(({
date,
days,
getEventsForDay,
getMultiDayEventsForDay,
dayWidth,
onPress,
screenWidth,
sortedDaysOfWeek
}: {
date: Date;
days: Date[];
getEventsForDay: (date: Date) => CalendarEvent[];
getMultiDayEventsForDay: (date: Date) => CalendarEvent[];
dayWidth: number;
onPress: (date: Date) => void;
screenWidth: number;
sortedDaysOfWeek: string[];
}) => {
const weeks = useMemo(() => {
const result = [];
for (let i = 0; i < days.length; i += 7) {
result.push(days.slice(i, i + 7));
}
return result;
}, [days]);
const eventPositions = useMemo(() => {
const positions = new Map<string, number>();
const weekTracking = new Map<number, Set<string>>();
weeks.forEach((week, weekIndex) => {
const activeEvents = new Set<string>();
week.forEach(day => {
const events = getMultiDayEventsForDay(day);
events.forEach(event => {
activeEvents.add(event.id);
});
});
weekTracking.set(weekIndex, activeEvents);
activeEvents.forEach(eventId => {
if (!positions.has(eventId)) {
const prevWeekEvents = weekIndex > 0 ? weekTracking.get(weekIndex - 1) : new Set<string>();
const usedPositions = new Set<number>();
if (prevWeekEvents) {
prevWeekEvents.forEach(prevEventId => {
if (activeEvents.has(prevEventId)) {
usedPositions.add(positions.get(prevEventId) || 0);
}
});
}
let position = 0;
while (usedPositions.has(position)) {
position++;
}
positions.set(eventId, position);
}
});
});
return positions;
}, [weeks, getMultiDayEventsForDay]);
return (
<View style={[styles.scrollView, {width: screenWidth}]}>
<View style={styles.monthHeader}>
<Text style={styles.monthText}>{format(date, 'MMMM yyyy')}</Text>
<View style={styles.weekDayRow}>
{sortedDaysOfWeek.map((day, index) => (
<Text key={index} style={styles.weekDayText}>{day}</Text>
))}
</View>
</View>
<View style={styles.daysGrid}>
{weeks.map((week, weekIndex) => (
<React.Fragment key={weekIndex}>
{week.map((date, dayIndex) => {
const multiDayEvents = getMultiDayEventsForDay(date).map(event => ({
...event,
weekPosition: eventPositions.get(event.id) || 0
}));
return (
<Day
key={`${weekIndex}-${dayIndex}`}
date={date}
events={getEventsForDay(date)}
multiDayEvents={multiDayEvents}
dayWidth={dayWidth}
onPress={onPress}
/>
);
})}
</React.Fragment>
))}
</View>
</View>
);
});
const HEADER_HEIGHT = 40;
const styles = StyleSheet.create({
multiDayContainer: {
position: 'absolute',
top: 29,
left: 0,
right: 0,
bottom: 0,
zIndex: 1,
},
dayContent: {
flex: 1,
padding: 4, // Move padding here instead
},
eventsContainer: {
flex: 1,
marginTop: 2,
position: 'relative',
},
event: {
borderRadius: 4,
padding: 1,
marginVertical: 1,
height: 14,
},
eventText: {
fontSize: 10,
color: '#fff',
fontWeight: '500',
},
day: {
height: '14%',
padding: 0,
borderWidth: 0.5,
borderColor: '#eee',
position: 'relative',
overflow: 'visible',
},
container: {
flex: 1,
height: '100%',
},
header: {
flexDirection: 'row',
justifyContent: 'center',
paddingHorizontal: 16,
paddingVertical: 8,
},
scrollView: {
flex: 1,
},
monthContainer: {
flex: 1,
},
daysGrid: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
// justifyContent: 'center'
},
weekDay: {
alignItems: 'center',
justifyContent: 'center',
height: HEADER_HEIGHT,
},
scrollContent: {
flex: 1,
},
weekDayText: {
fontSize: 12,
fontWeight: '600',
color: '#666',
},
dateContainer: {
minWidth: 20,
height: 24,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 12,
},
todayContainer: {
backgroundColor: '#6200ee',
},
dateText: {
fontSize: 12,
fontWeight: '500',
color: '#333',
},
todayText: {
color: '#fff',
},
outsideMonthText: {
color: '#ccc',
},
moreEvents: {
fontSize: 10,
color: '#666',
textAlign: 'center',
},
monthHeader: {
paddingVertical: 12,
},
monthText: {
fontSize: 16,
fontWeight: '600',
color: '#333',
textAlign: 'center',
marginBottom: 8,
},
weekDayRow: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingHorizontal: 0,
},
});
export default MonthCalendar;

View File

@ -1,13 +1,27 @@
import { atom } from "jotai";
import * as Device from "expo-device";
import { CalendarEvent } from "@/components/pages/calendar/interfaces";
const getDefaultMode = () => {
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
return isTablet ? "week" : "3days";
};
export const editVisibleAtom = atom<boolean>(false);
export const isAllDayAtom = atom<boolean>(false);
export const eventForEditAtom = atom<CalendarEvent | undefined>(undefined);
export const isFamilyViewAtom = atom<boolean>(false);
export const modeAtom = atom<"week" | "month" | "day">("week");
export const modeAtom = atom<"week" | "month" | "day" | "3days">(getDefaultMode());
export const selectedDateAtom = atom<Date>(new Date());
export const selectedNewEventDateAtom = atom<Date | undefined>(undefined);
export const settingsPageIndex = atom<number>(0);
export const userSettingsView = atom<boolean>(true);
export const toDosPageIndex = atom<number>(0);
export const refreshTriggerAtom = atom<boolean>(false);
export const refreshEnabledAtom = atom<boolean>(true);
export const selectedUserAtom = atom<{
uid: string;
firstName: string;
lastName: string;
eventColor?: string;
} | null>(null);

View File

@ -1,7 +1,8 @@
export const modeMap = new Map([
[0, "day"],
[1, "week"],
[2, "month"],
[1, "3days"],
[2, "week"],
[3, "month"]
]);
export const months = [

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