237 Commits

Author SHA1 Message Date
Nicholas Pease e9948c1f5c Fixed DM's (#71) 2024-04-07 13:03:53 -04:00
npease 568628f0a2 Fixed DM's 2024-04-07 03:56:14 -04:00
Nicholas Pease abf46c6885 Add to both (#70) 2024-04-07 03:27:37 -04:00
npease 01d8598f59 Add to both 2024-04-07 03:26:10 -04:00
Nicholas Pease 1095b781cd Hotfix for Profiles (#69) 2024-04-07 03:19:14 -04:00
npease 1569d3c7ce Hotfix for Profiles 2024-04-07 03:18:17 -04:00
Nicholas Pease 652b35f5bd Add Friends / DM's V1 (#68) 2024-04-07 03:08:48 -04:00
ClarkLach 57a8af7ef8 Added user list on hover (#67) 2024-04-07 03:08:11 -04:00
npease e8be631182 Fixes for Login Bug 2024-04-07 03:07:22 -04:00
npease 1dda82790c DM's Working 2024-04-07 03:05:16 -04:00
ClarkLach c47ea53b50 Added user list on hover
List all users of room (on sidebar) when hovering. Could use a little formatting more probably.
2024-04-07 02:20:01 -04:00
npease d4d994fe26 Friending V1 2024-04-07 00:38:07 -04:00
Nicholas Pease 2dc2f380ff Add Ionic Capacitor Support (Cross Platform Native) (#62) 2024-04-06 20:51:29 -04:00
Nicholas Pease 2604fc2034 Profile Visual Fix (#65) 2024-04-06 20:51:14 -04:00
Nicholas Pease 7db4b2a2ee Login / Register Verification (#66) 2024-04-06 20:46:42 -04:00
Nicholas Pease 3ec8263d15 Add Online / Total to Room List (#64) 2024-04-06 20:44:58 -04:00
npease d0c14cf1ab Login / Register Verification 2024-04-06 17:55:11 -04:00
npease a078c9adec Profile Visual Fix 2024-04-06 17:14:08 -04:00
npease 3caa4dcde7 Remove console logs 2024-04-06 17:00:23 -04:00
npease 860c6d1075 Add Online / Total to Room List 2024-04-06 16:57:16 -04:00
ClarkLach cccfd98736 Map Markers Fixed (#63) 2024-04-06 01:48:47 -04:00
Nicholas Pease c37d521d0e Merge branch 'main' into clarkl-apr5 2024-04-06 01:47:44 -04:00
ClarkLach 1de58de8de Room/User Links on Map 2024-04-06 01:46:32 -04:00
ClarkLach fc1adb5662 Main Page Markers are back.
Still a bit to do here.
2024-04-06 01:32:11 -04:00
Nicholas Pease b5e36872b6 Merge branch 'main' into npease-capacitor-v1 2024-04-06 01:07:26 -04:00
npease 1c4183dd85 Final touches for V1 2024-04-06 01:06:17 -04:00
Nicholas Pease 535302c893 Add URL Detection (#60) 2024-04-06 01:05:45 -04:00
Nicholas Pease 3a28cc392a Fix Bad Merge 2024-04-06 01:04:50 -04:00
npease 63321e0550 Restore unauth redirects 2024-04-06 00:28:43 -04:00
npease 4789da2c43 Init Android 2024-04-06 00:24:43 -04:00
ClarkLach 1498e2615c Actually Fixed next/babel error 2024-04-05 22:59:44 -04:00
ClarkLach 3aad970fd6 next/babel error fix 2024-04-05 22:55:00 -04:00
Nicholas Pease c5821737b2 Merge branch 'main' into npease-rich-messages-v1 2024-04-05 22:35:28 -04:00
Nicholas Pease 75a79711aa Update Chat/App/Profile Pages to be Mobile Friendly (#61) 2024-04-05 22:33:08 -04:00
npease f4447fd2dc Fixes to Send Box in Chat Messages 2024-04-05 21:19:09 -04:00
npease 6be9999d44 WIP 2024-04-05 20:31:35 -04:00
npease 5dd4bffdc5 Initial Setup 2024-04-05 19:38:10 -04:00
npease 14dfdf8735 Fix for Sidebar Width 2024-04-05 22:20:43 +00:00
npease 19eed3810d First Param Fix 2024-04-04 20:53:50 -04:00
npease d5e0f3ca79 Make User Profile Static 2024-04-04 20:51:13 -04:00
npease b868d67cd1 Mobile Ready Chat Page 2024-04-05 00:18:47 +00:00
npease 098a0f469d App Page Mobile Ready 2024-04-04 23:45:05 +00:00
npease 718c489042 Profile Page Mobile Ready 2024-04-04 21:56:23 +00:00
npease 94c8b900a9 Add URL Detection 2024-04-04 21:32:24 +00:00
Nicholas Pease 830d7f75e1 Made Recurse 2024-04-04 00:24:13 -04:00
Nicholas Pease 3ec8386a04 Restore Rooms User Leave/Enter Status (#59) 2024-04-04 00:16:34 -04:00
npease a1e1f28137 Restore Rooms User Leave/Enter Status 2024-04-04 04:16:08 +00:00
Nicholas Pease f6873efcdd Delete vercel.json 2024-04-03 23:49:20 -04:00
Nicholas Pease c4be27dada Move vercel.json 2024-04-03 23:48:58 -04:00
Nicholas Pease b319c0e6e4 Create vercel.json 2024-04-03 23:40:27 -04:00
Nicholas Pease c63383ba89 Update Config to work with JsDoc 2024-04-03 23:29:14 -04:00
Nicholas Pease df62f8f846 Refactor / Documentation for All Functions / Components (#58) 2024-04-03 22:53:20 -04:00
Nicholas Pease 17d895cab0 Add Notifications V1 (#57) 2024-04-03 22:52:26 -04:00
Nicholas Pease 86cfdfb875 Convert To Link for Preload Speed Advantage (#55) 2024-04-03 22:52:01 -04:00
npease 370c7c39f5 Add jsdoc workflow 2024-04-04 02:03:32 +00:00
npease 5dd891c5b2 JS Docs for all Files 2024-04-04 01:39:08 +00:00
npease 7bf163d77c Move Profile Panel to Profile Component Directory 2024-04-03 23:36:11 +00:00
npease 9feead28cd Slight Change 2024-04-03 23:27:30 +00:00
npease 323b7b555a Notification Functions Added 2024-04-03 23:25:47 +00:00
npease d09a527c9d Change homepage coords to new Geolocated import 2024-04-03 22:21:29 +00:00
Nicholas Pease 25a112bc3f Dev Merge (#56) 2024-04-03 18:10:27 -04:00
npease b7ec08f5ef Final Touches 2024-04-03 22:08:25 +00:00
npease 8bca33a039 Good to Release 2024-04-03 22:04:19 +00:00
npease 6179f8d4c2 Initial 2024-04-03 21:52:55 +00:00
npease e08f4591c6 Rename / Transition to Headless UI Tab Control 2024-04-03 01:18:44 +00:00
Nicholas Pease bc04d35909 Refactor / Lag Fix (#54) 2024-03-31 13:46:06 -04:00
npease 5d8a29d19c Final Touches 2024-03-31 05:22:10 +00:00
npease b52c17162d Most functions working 2024-03-31 05:08:52 +00:00
npease 3a555a690c In Progress Home 2024-03-31 03:01:19 +00:00
npease d81ba003e0 Split chat into page 2024-03-31 02:28:57 +00:00
Nicholas Pease c4292fdd33 Undo Bad Merge (#53) 2024-03-30 20:29:11 -04:00
Nicholas Pease 13d04ad364 Undo Bad Merge 2024-03-30 20:28:11 -04:00
ClarkLach 525fe38b73 Restructing Changes + Bugfixes (#51) 2024-03-30 20:26:15 -04:00
ClarkLach e1e9fb877a Added Deliverable 3 (#50) 2024-03-30 20:26:01 -04:00
ClarkLach b826f1f6a4 Commenting
A lil more descriptive, changed a function name as well
2024-03-30 20:24:40 -04:00
ClarkLach e2a8c75d46 Profile Edit into Component
Continuing to break up functionality into separate files
2024-03-30 02:19:49 -04:00
ClarkLach a4fc9bdce0 More Restructure, Formatting all file contents
Breaking stuff into components piece by piece. Also used Prettier on all files in one commit. (Should stick to a consistent, readable format !!)
2024-03-30 01:30:36 -04:00
ClarkLach 1a6700a983 Fixed Broken Profile Page
Accidentally broke profile page (since I'm moving the map functions around)
2024-03-30 01:07:40 -04:00
ClarkLach 2571b8cc73 CSS Fixes
Got rid of all the errors of duplicate colors/sizing.
2024-03-30 00:55:11 -04:00
ClarkLach 88b7202069 Links for room markers
Room Marker links to room, deleted unneeded files, beginning work on better structuring
2024-03-30 00:50:38 -04:00
ClarkLach 456c8bc1af Fixed Profile Name and updated README 2024-03-29 11:25:58 -04:00
ClarkLach c36b33402b Added Proposal Presentation 2024-03-28 14:08:49 -04:00
ClarkLach abdbe69fd9 Deleted filler file 2024-03-28 14:08:18 -04:00
ClarkLach 81da8f5b2d Added Deliverable 3 files 2024-03-28 14:07:53 -04:00
ClarkLach b05bebfd07 Create deliverable_3 folder 2024-03-28 14:06:17 -04:00
Nicholas Pease 947041b62b User Profiles V1 (#49) 2024-03-28 14:02:00 -04:00
npease e50062f615 Fix for display sizing 2024-03-28 02:49:37 +00:00
npease ced20c16c5 Final Fixes 2024-03-27 18:24:55 +00:00
npease a2ae2b2bea Finishing up first profile version 2024-03-27 16:53:37 +00:00
npease f82b04d36c Custom Profile Picture Upload Complete 2024-03-27 04:36:19 +00:00
npease fdb22a5307 Progress Commit 2024-03-27 04:11:37 +00:00
Nicholas Pease f92bf2510a Merge Latest to Dev (#46) 2024-03-24 01:26:48 -04:00
npease 965db39ad0 Profiles IP, Page Created 2024-03-24 05:25:07 +00:00
Nicholas Pease 584510d0f3 Refactor Authentication Flow (#44) 2024-03-23 16:54:19 -04:00
Nicholas Pease 981d17166e Dev Branch Update (#45) 2024-03-22 16:12:23 -04:00
npease 12cfc6f6a0 Remove Dependency 2024-03-22 20:05:11 +00:00
npease 2ec3b93190 Polished Off, Bloatware Reduced 2024-03-22 06:24:12 +00:00
npease 961e2b4587 Polishing Touches, Remove of Unnecessary Files, Register -> Onboard 2024-03-22 05:42:35 +00:00
npease eaadade0ed Mostly There, Fine Tuning Required 2024-03-22 04:59:40 +00:00
npease 564524772e Login Refactored, TODO: Delete old API and other references 2024-03-21 22:55:41 +00:00
npease b2c21d0782 Finalize THIS refactor 2024-03-21 22:28:27 +00:00
npease fa29041489 Cleanup 2024-03-21 18:28:57 +00:00
npease 296f21a082 Initial Refactor 2024-03-21 18:22:43 +00:00
ClarkLach ed3449a1f7 Added Username Colors in Chats (#41) 2024-03-19 14:25:16 -04:00
ClarkLach 9f84ee233d Merge branch 'main' into clarkl-mar14 2024-03-19 14:23:57 -04:00
Nicholas Pease 1396d03b76 Fix Borked Nearby Update (#40) 2024-03-19 14:20:00 -04:00
Nicholas Pease ba0575f315 Merge branch 'main' into npease-hotfix-39 2024-03-14 18:39:09 -04:00
ClarkLach 644206635f Added username colors in chats
Ideally this will be customizable at some point
2024-03-14 18:31:05 -04:00
ClarkLach 2b6bc641d4 Update .gitignore
DS_store activity
2024-03-14 17:13:16 -04:00
ClarkLach 0962df2187 Uploaded Deliverables and Deleted Old Files (#35) 2024-03-14 16:54:32 -04:00
npease 7212a532f6 Fix Borked Nearby Update 2024-03-12 04:28:46 +00:00
Nicholas Pease 85cd7c20f4 Add Nearby Room Counter for Unauthenticated / New Users (#38) 2024-03-12 00:22:01 -04:00
Nicholas Pease 844bb14040 Add Online, Offline, Saving Capabilities (#36) 2024-03-12 00:21:36 -04:00
Nicholas Pease 2e0ac0ba0f Merge branch 'npease-profiles-v1' into npease-chatrooms-profile 2024-03-12 00:19:43 -04:00
npease ea3647ecd3 Rooms appear on big map 2024-03-10 20:25:20 +00:00
npease 2796172d30 Fix Maps for Chatroom 2024-03-10 20:04:10 +00:00
npease 0c8ca18326 Add counter for unauthenticated users on homepage to show rooms nearby 2024-03-10 17:39:08 +00:00
npease b1009b186e Fix Added Members, onValue for Room List, Nearby 2024-03-10 16:43:08 +00:00
Nicholas Pease 72ae128975 Merge branch 'main' into npease-chatrooms-profile 2024-03-10 01:29:55 -05:00
npease 9802ea8096 Final myRooms Fix 2024-03-10 06:27:31 +00:00
npease aa49341937 myRooms Working 2024-03-10 06:10:39 +00:00
npease 29d6833e42 Initial Working Online/Offline Members 2024-03-10 04:22:42 +00:00
npease d26e7d4290 WIP 2024-03-06 04:26:36 +00:00
npease 1f22895904 Prelim Chat Sidebar Data Working 2024-03-05 06:01:28 +00:00
npease 319f066edf Main App Page Refactor, Commenting, Chat Room Sidebar Work 2024-03-05 05:38:23 +00:00
npease c77f16d3fc UI Worked On, Need to Pass Chatroom Path Data to sidebar somehow 2024-03-04 05:30:42 +00:00
ClarkLach c8d7e40203 Update README.md
fixed missing command
2024-03-03 23:56:01 -05:00
ClarkLach e1572cd84a Delete .github/workflows directory 2024-03-03 23:55:09 -05:00
ClarkLach 1e5f462abd Delete deliverables/deliverable_2/filler.txt 2024-03-03 23:53:40 -05:00
ClarkLach 973096d480 Delete deliverables/deliverable_1/filler.txt 2024-03-03 23:53:28 -05:00
ClarkLach 5aa1c888e1 Delete deliverables/deliverable_0/filler.txt 2024-03-03 23:53:15 -05:00
ClarkLach 594c09ba65 Delete deliverables/filler.txt 2024-03-03 23:53:04 -05:00
ClarkLach a883e3112e Uploaded deliverable 1 files 2024-03-03 23:52:29 -05:00
ClarkLach b87c6ee600 Uploaded Deliverable 0 Files 2024-03-03 23:50:40 -05:00
ClarkLach 94b8591643 Create deliverable 0 2024-03-03 23:50:11 -05:00
ClarkLach b7a30c3d6d Create deliverable 1 2024-03-03 23:49:56 -05:00
ClarkLach daa9de4142 Uploaded Deliverable 2 files 2024-03-03 23:48:15 -05:00
ClarkLach 08b57dc2b2 Create deliverable 2 2024-03-03 23:47:42 -05:00
ClarkLach e179e8cdef Create deliverables directory 2024-03-03 23:46:58 -05:00
ClarkLach 57e61e362a Delete backend directory 2024-03-03 23:45:55 -05:00
npease 0042244b80 Prelim Profile 2024-03-03 03:17:53 +00:00
Nicholas Pease e8dfc7bf88 Fix Homepage Buttons Loading Slow (#31) 2024-03-02 21:23:07 -05:00
Nicholas Pease 72bcccfe2f Add Preliminary Chatroom Support (#32)
Add Preliminary Chatroom Support
2024-03-02 21:22:53 -05:00
Sgoodridge96 2c457ab531 Fixed bugs from last PR: Updated register page for password confirmation (#30) 2024-02-27 15:27:54 -05:00
Nicholas Pease 1120f5eb6e Merge branch 'main' into sgoodridge-edit1 2024-02-26 21:12:43 -05:00
Nicholas Pease f16b77c94b Merge branch 'main' into npease-homepage-fix 2024-02-26 18:34:29 -05:00
Nicholas Pease 62a813d0ad Add Username to Onboarding, Fix Onboarding (#28) 2024-02-26 14:59:27 -05:00
Stephen 151de4ebfd updated register page for password confirmation: Fixed bugs with last PR 2024-02-26 14:22:04 -05:00
Sgoodridge96 199c283b0e Update page.js
Updated errors from pull request:
Added a password confirmation to register page
2024-02-26 13:37:34 -05:00
npease 60bdf36274 Chatrooms: Nearby Lookup Working 2024-02-26 05:40:11 +00:00
npease a7eb9b942b Minimal Functional Chatrooms 2024-02-25 23:46:41 +00:00
npease 7cd1e9b5da WIP: Message Fetch Works 2024-02-25 08:38:40 +00:00
npease afd5dbaa9f Restructure App, Created Chat UI, Ready for Firebase Integration / Backend 2024-02-25 03:19:12 +00:00
npease eeb6b856e6 UI Changes, Prepare for Chatrooms 2024-02-24 06:35:23 +00:00
npease 9ee8bf3376 Add username, fix onboarding 2024-02-24 02:15:53 +00:00
npease 848d588bf4 Fix Homepage Buttons Loading Slow 2024-02-23 20:02:08 +00:00
Stephen 7ca4b62848 Added password confirmation to register page 2024-02-23 14:05:14 -05:00
Stephen 8cd5fd8783 Added password confimation during registration 2024-02-23 14:02:34 -05:00
Stephen afd72ec72b Added password confirmation on register page 2024-02-23 13:52:38 -05:00
Nicholas Pease 0895e93f6c Optimize Authentication Flow, Better Visual Feedback (#25) 2024-02-23 12:09:52 -05:00
Stephen 67ec566728 Added re enter password to register page 2024-02-23 11:58:09 -05:00
npease 8737d10a1e If user is authenticated, redirect to app from /login and /register pages 2024-02-23 04:37:03 +00:00
npease d9bca7f1ff Loading Icon on Login/Register button press 2024-02-23 04:22:36 +00:00
npease 4b9b46f10d Improved Onboarding User Verification, Removed Login/Register Buttons on Homepage for Logged In Users 2024-02-23 04:09:45 +00:00
npease bafcd88fa1 Optimize User Info Storage & Reduce API Calls to DB 2024-02-23 03:44:05 +00:00
Nicholas Pease ac7317a0b7 Validation on Register / Remove Download Button From Homepage (#23) 2024-02-22 14:08:14 -05:00
Nicholas Pease 034b217916 Update README.md with new URL (#21)
Update README.md with new URL
2024-02-22 14:06:53 -05:00
Nicholas Pease ef3f7fa174 Login: Invalid Username / Password Prompts User (#22) 2024-02-22 14:06:31 -05:00
npease 17d2ce436a Delete Download Button 2024-02-22 19:00:47 +00:00
npease 6dbc0a2e8e Register Page Validation 2024-02-22 18:59:57 +00:00
npease 43e9045b0a Final CSS Changes 2024-02-22 18:24:25 +00:00
npease 5ca9c4222c Slight CSS changes to keep login page from scrolling 2024-02-22 18:22:55 +00:00
npease 5e8a5c89b3 Add red border on error to login 2024-02-22 18:18:59 +00:00
Nicholas Pease dc5469fc70 Update README.md with even newer URL 2024-02-22 12:09:38 -05:00
npease ac19919c51 Login: Invalid Username / Password Prompts User 2024-02-22 06:17:53 +00:00
Sgoodridge96 7d2b653953 Change button Colors (#20) 2024-02-21 09:29:54 -05:00
Nicholas Pease 6dea6cc168 Update README.md with new URL 2024-02-21 09:14:05 -05:00
Nicholas Pease 75e3476d48 Load default map then center on users computed location (#16) 2024-02-21 09:08:53 -05:00
Stephen c73f85a411 Merge branch 'main' of https://github.com/ChatMaps/ChatMaps into sgoodridge 2024-02-20 23:33:18 -05:00
Nicholas Pease 5aba1549d4 Deployment Fixes (#19) 2024-02-20 22:56:49 -05:00
npease 3f68e43efd Merge branch 'main' of https://github.com/LAX18/ChatMaps 2024-02-20 22:55:22 -05:00
npease b24a2b5254 Remove Logging 2024-02-20 22:55:20 -05:00
Nicholas Pease b38f12a6ab Merge branch 'ChatMaps:main' into main 2024-02-20 22:54:51 -05:00
npease b07a333459 Cookie Deployment Fix 2024-02-20 22:51:09 -05:00
Stephen 13e5f319c7 changes to buttons 2024-02-20 22:17:35 -05:00
npease 4391a072cc Different way of setting cookies 2024-02-20 17:33:06 -05:00
npease 66c9de922d New way of setting cookies 2024-02-20 17:27:01 -05:00
npease 245ae616b1 Testing fixes with cookie on middleware 2024-02-20 17:05:01 -05:00
Nicholas Pease 3afbe17a21 Fix Deployment Authentication Problems (#18) 2024-02-20 16:49:23 -05:00
Nicholas Pease 8d3283ef04 Merge branch 'ChatMaps:main' into main 2024-02-20 16:48:13 -05:00
npease 8862f7b94c Fix login on deployment 2024-02-20 16:47:51 -05:00
Nicholas Pease 6e5af78586 Merge branch 'main' into npease-ui-maps-fix 2024-02-20 16:39:24 -05:00
Nicholas Pease ca731147f2 Move Firebase Auth to .env files to support Vercel deployments (#17) 2024-02-20 16:33:12 -05:00
npease a8dc4d8460 Move to .env.local files instead of files on dir 2024-02-20 16:26:01 -05:00
npease 6a0d3f3834 Add marker on user location (keep?) 2024-02-20 15:08:59 -05:00
npease 04cdc500b2 Load default map then center on users computed location 2024-02-20 14:59:33 -05:00
Nicholas Pease 8cd7cafdb5 Add Initial UI and Authentication (#11) 2024-02-20 14:34:40 -05:00
npease ec2fc15a3f Add onboarding, dashboard with relevant API's 2024-02-20 01:08:12 -05:00
Nicholas Pease d7a2382cb5 Restore Dependency Installation 2024-02-19 16:37:16 -05:00
npease 69d5bfe9a9 Remove build stage to remove conflict with local files 2024-02-19 21:34:08 +00:00
npease c555a59cf8 Spaces Matter 2024-02-19 21:26:32 +00:00
npease d20aecdbe8 Use GH Secrets with Workflow 2024-02-19 21:24:45 +00:00
npease 57a8415e52 Update Build Test 2024-02-19 21:02:14 +00:00
npease f19b09c5fd Update Import Paths 2024-02-19 08:17:17 +00:00
npease c6056c385b Favicon Fixes / Cleanup 2024-02-19 08:06:16 +00:00
npease 7420cc63fb Cleanup/condense package.json files 2024-02-19 07:59:18 +00:00
npease c528c6bacf Refactor / Commentate 2024-02-19 07:50:37 +00:00
npease daedd0b068 Initial UI, Login/Register Flow Prelim, Icons 2024-02-19 06:40:11 +00:00
ClarkLach f83612634d Merge pull request #10 from ChatMaps/clarkl
Updated README, testing page design
2024-02-18 21:26:54 -05:00
ClarkLach b7138e5df8 JS Testing 2024-02-18 18:28:16 -05:00
ClarkLach 5f63e59b24 Update README
Updated README and fixed .gitignore
2024-02-18 17:37:48 -05:00
ClarkLach a8254095d7 Create .gitignore 2024-02-18 17:29:38 -05:00
Nicholas Pease dde81c6f9f Merge pull request #8 from ChatMaps/npease-server-update-hook
Backend Auto Update Server
2024-02-18 17:00:14 -05:00
Nicholas Pease 2e5340e775 Merge pull request #6 from ChatMaps/5-npease-restore-original-readme
Restore Original README
2024-02-18 14:35:03 -05:00
Nicholas Pease 92764da0da Merge pull request #4 from ChatMaps/npease-folder-restructure
Restructure Project Files into Distinct Folders
2024-02-18 14:34:40 -05:00
npease cbae67c36b Checkout main 2024-02-18 04:06:45 +00:00
npease 01f37ac46b Cleanup and Docs 2024-02-18 04:03:49 +00:00
npease f39b47b790 Add frontend systemd file 2024-02-18 03:28:31 +00:00
npease c10d496c57 Test logic for restart / update 2024-02-18 03:03:35 +00:00
npease cec4fac22f Add systemd file 2024-02-18 02:54:25 +00:00
npease a316c92ff9 Add conditional logic for PR closed, merged and to main 2024-02-18 02:39:27 +00:00
npease c86d9275e7 Restructure Folder Structure 2024-02-18 02:19:01 +00:00
npease fd82639658 Proper routing and parsing for test condition 2024-02-18 02:09:01 +00:00
npease 9da1b6c78c Extend post route 2024-02-18 01:42:44 +00:00
npease c5ba2fbcf8 Webhook shell, ready for server testing 2024-02-18 01:28:40 +00:00
npease f49c18f712 Update frontend tests 2024-02-18 01:03:01 +00:00
Nicholas Pease c3befbcebd Restore original README 2024-02-17 00:05:54 -05:00
Nicholas Pease 8d25628947 Merge branch 'main' into npease-folder-restructure 2024-02-16 23:51:58 -05:00
npease cbaf8cae3a Typo in final step of frontend-tests.yml 2024-02-17 04:50:44 +00:00
Nicholas Pease d05153ce68 Merge pull request #3 from ChatMaps/npease-add-nextjs-workflow
Add nextjs frontend tests
2024-02-16 23:46:33 -05:00
npease 9beb3135fb Restructure Project Files into Distinct Folders 2024-02-17 04:40:09 +00:00
Nicholas Pease e251cd7481 Add nextjs frontend tests 2024-02-16 23:35:43 -05:00
JGCS22 0147d683ec App-Template-P2 2024-02-15 14:45:41 -05:00
JGCS22 3067f2f4ef App-Template 2024-02-15 14:41:41 -05:00
ClarkLach 4db45ad718 Update README.md 2024-01-26 14:56:46 -05:00
ClarkLach 849da09805 Update README.md 2024-01-26 14:56:28 -05:00
ClarkLach de00f99b8b Update README.md 2024-01-20 21:18:26 -05:00
90 changed files with 13715 additions and 2 deletions
+27
View File
@@ -0,0 +1,27 @@
name: JSDoc to GH Pages
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Build
uses: andstor/jsdoc-action@v1
with:
source_dir: ./frontend-next
output_dir: ./jsdoc
recurse: true
template: minami
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./jsdoc
+140
View File
@@ -0,0 +1,140 @@
# Android Build
/frontend-next/android
# Firebase Stuff
firebase-admin-key.json
firebase*.json
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Am I the only mac user
**/.DS_Store
+27 -2
View File
@@ -1,2 +1,27 @@
# COS420-Project
Main repo for COS420 Project Group 4
![](/frontend-next/public/logos/logo_transparent.png)
Main repo for ChatMaps, our COS420 Project.
ChatMaps is a web-based social networking service that allows users to connect to others in their local geographic area. It implements an interactable mapping utility to show general user locations relative to other users, as well as a chat room feature that allows users to start public conversations based on a specified topic. ChatMaps is primarily intended for use in densely populated areas, such as college campuses or metropolitan areas, so people of similar interests can start conversations. The goal of this project is to create a web app that plots locations, gives a radius of the local area, and connects users into different topic-based chat rooms.
This service implements user login and profiles, allowing users to add each other as friends and start private conversations. There are several default chat rooms of varying topics, but users also have the ability to create their own topics that will be viewable by other users. For example, a user at the University of Maine could create a joinable chat room titled “COS420”, which would be visible to others near this campus.
This app shares some similarities to other social networks that implement location-based content. ChatMaps novel approach is to utilize user location to facilitate real-time communication with others within a given radius.
The live version of this app can be found at:
https://chatma.ps/
A local version can be run with:
cd frontend-next/
npm install
npm run build
npm run start
then navigating to:
http://localhost:3000
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

+9
View File
@@ -0,0 +1,9 @@
{
"extends": ["next/babel","next/core-web-vitals" ],
"rules": {
"no-unused-vars": ["warn", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }],
"jsx-a11y/alt-text": "off",
"@next/next/no-img-element": "off",
"no-console": 1
}
}
+12
View File
@@ -0,0 +1,12 @@
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.jacsn.chatmaps',
appName: 'ChatMaps',
webDir: 'out',
server: {
androidScheme: 'https'
}
};
export default config;
+21
View File
@@ -0,0 +1,21 @@
import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getDatabase} from "firebase/database"
import {getStorage} from "firebase/storage"
var config = {
apiKey: "AIzaSyDbDPjQGt-lIjNPeTG-Q5AECM1m0vtOr2c",
authDomain: "chatmaps-3e7fa.firebaseapp.com",
projectId: "chatmaps-3e7fa",
storageBucket: "chatmaps-3e7fa.appspot.com",
messagingSenderId: "771010649524",
appId: "1:771010649524:web:b6e66d3457820c817b26e1",
databaseURL: "https://chatmaps-3e7fa-default-rtdb.firebaseio.com/",
}
var app = getApps().length > 0 ? getApp() : initializeApp(config);
var auth = getAuth(app);
var storage = getStorage(app);
var database = getDatabase(app);
export { auth, app, database, storage };
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

+7
View File
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
+4
View File
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {output: 'export'};
export default nextConfig;
+10575
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
{
"name": "chatmaps",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "npx serve@latest out",
"lint": "next lint"
},
"dependencies": {
"@capacitor/android": "^5.7.4",
"@capacitor/core": "^5.7.4",
"@capacitor/geolocation": "^5.0.7",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@headlessui/react": "^1.7.18",
"@mui/icons-material": "^5.15.14",
"@mui/material": "^5.15.14",
"firebase": "^10.6.0",
"next": "^14.1.0",
"pigeon-maps": "^0.21.3",
"react": "^18.2.0",
"react-beforeunload": "^2.6.0",
"react-dom": "^18.2.0",
"react-firebase-hooks": "^5.1.1",
"react-hook-form": "^7.50.1"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@capacitor/cli": "^5.7.4",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"postcss": "^8",
"tailwindcss": "^3.3.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+47
View File
@@ -0,0 +1,47 @@
{
"icons": [
{
"src": "../icons/icon-48.webp",
"type": "image/png",
"sizes": "48x48",
"purpose": "any maskable"
},
{
"src": "../icons/icon-72.webp",
"type": "image/png",
"sizes": "72x72",
"purpose": "any maskable"
},
{
"src": "../icons/icon-96.webp",
"type": "image/png",
"sizes": "96x96",
"purpose": "any maskable"
},
{
"src": "../icons/icon-128.webp",
"type": "image/png",
"sizes": "128x128",
"purpose": "any maskable"
},
{
"src": "../icons/icon-192.webp",
"type": "image/png",
"sizes": "192x192",
"purpose": "any maskable"
},
{
"src": "../icons/icon-256.webp",
"type": "image/png",
"sizes": "256x256",
"purpose": "any maskable"
},
{
"src": "../icons/icon-512.webp",
"type": "image/png",
"sizes": "512x512",
"purpose": "any maskable"
}
],
"background_color": "#ffffff"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

+17
View File
@@ -0,0 +1,17 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Home",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
+106
View File
@@ -0,0 +1,106 @@
"use client";
// System Imports
import { useState, useEffect } from "react";
// Dependencies
import Drawer from '@mui/material/Drawer';
// Firebase Imports
import { auth, database } from "../../../firebase-config";
import { ref, onValue } from "firebase/database";
import { useAuthState } from "react-firebase-hooks/auth"
// Component Imports
import { Header } from "../../components/app/header";
import { HomePage } from "../../components/app/page/home";
import { Sidebar } from "../../components/app/sidebar/home";
import {useWindowSize} from "../../components/app/datatypes";
// Capacitor Import
import { Geolocation } from '@capacitor/geolocation';
/**
* Contains most everything for the app homepage
* @returns {Object} Home Page
*/
function Home() {
// State variables for app page
const [user, setUser] = useState(null); // user data
const [loadingLoc, setLoadingLoc] = useState(true); // location variable loading, true = loading, false = finished loading
const [authUser] = useAuthState(auth) // auth user object (used to obtain other user object)
const [drawerOpen, setDrawerOpen] = useState(true); // drawer open state
const [coords, setCoords] = useState(null)
var windowSize = useWindowSize()
useEffect(() => {
if (windowSize.width < 767) {
setDrawerOpen(false)
} else {
setDrawerOpen(true)
}
}, [windowSize])
// Authentication Verification / Redirection if Profile Data not Filled out
useEffect(() => {
if (authUser) {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
setUser(userData);
} else {
window.location.href = "/onboarding";
}
});
}
}, [authUser])
useEffect(() => {
Geolocation.getCurrentPosition().then((position) => {
setCoords(position.coords);
setLoadingLoc(false);
});
}, [])
return (
<div className="overflow-hidden h-dvh">
{user && (
<div className="overflow-hidden h-dvh">
{/* Left Side of Page */}
<div className="overflow-hidden h-dvh md:mr-[405px]">
{/* Header */}
<Header
mainTab={"home"}
user={user}
sidebarControl={() => {setDrawerOpen(!drawerOpen)}}
/>
{/* Main Page Section */}
<div className="mr-2 h-[calc(100%-110px)]">
{!loadingLoc && (
<HomePage loc={coords} user={user} />
)}
{loadingLoc && (
<HomePage loc={null} user={user} />
)}
</div>
</div>
{/* Sidebar (Right Side of Page) */}
<Drawer open={drawerOpen} anchor={"right"} variant={windowSize.width > 767? "persistent": "temporary"} onClose={() => {setDrawerOpen(false)}} sx={{
width: windowSize.width > 767? 400: "80%",
marginTop: 10,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: windowSize.width > 767? 400: "80%",
borderLeft: 0,
},
}}>
<div className="shadow-2xl">
<Sidebar user={user} location={coords} loadingLoc={loadingLoc}/>
</div>
</Drawer>
</div>
)}
</div>
);
}
export default Home;
+17
View File
@@ -0,0 +1,17 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Chat",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
+139
View File
@@ -0,0 +1,139 @@
"use client";
// System Imports
import Drawer from '@mui/material/Drawer';
import { useState, useEffect } from "react";
// Firebase Imports
import { auth, database } from "../../../firebase-config";
import { ref, onValue, set, onDisconnect } from "firebase/database";
import { useAuthState } from "react-firebase-hooks/auth"
// Component Imports
import { Header } from "../../components/app/header";
import { ChatRoom } from "../../components/app/page/chat";
import { Sidebar } from "../../components/app/sidebar/chat";
import {useWindowSize} from "../../components/app/datatypes";
/**
* Chat Page
* @returns {Object} Chat Page
*/
function Chat() {
// State variables for chat page
const [user, setUser] = useState(null); // user data
const [chatRoomObj, setChatRoomObj] = useState(null); // Current chatroom object
const [doneLoading, setDoneLoading] = useState(false) // is the page done loading or not
const [authUser] = useAuthState(auth) // auth user object (used to obtain other user object)
const [drawerOpen, setDrawerOpen] = useState(true); // drawer open state
var windowSize = useWindowSize()
useEffect(() => {
if (windowSize.width < 767) {
setDrawerOpen(false)
} else {
setDrawerOpen(true)
}
}, [windowSize])
// Authentication Verification / Redirection if Profile Data not Filled out
useEffect(() => {
if (authUser) {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
setUser(userData);
} else {
window.location.href = "/onboarding";
}
});
}
}, [authUser])
// Users URL params to load proper chatroom, then logs the user into that room
useEffect(() => {
if (user) {
const searchParams = new URLSearchParams(document.location.search);
var path = searchParams.get("room")
/*// Send entered message
var payload = {
body: "entered",
user: user.username,
isSystem: true,
timestamp: new Date().getTime(),
uid: user.uid,
};
set(
ref(
database,
`/rooms/${path}/chats/${new Date().getTime()}-${user.username}`
),
payload
);*/
// Add user to online for room
set(ref(database, `/rooms/${path}/users/online/${user.uid}`), user)
// Removes user from room on disconnect (reload, window close, internet lost)
onDisconnect(ref(database, `/rooms/${path}/users/online/${user.uid}`)).remove()
// Sends leaving message on disconnect (Timestamp function used due to new onDisconnect stuff)
/*someRef = ref(database, `/rooms/${path}/chats/${new Date().getTime()}-${user.username}`)
onDisconnect(someRef).set({
body: "left",
user: user.username,
isSystem: true,
timestamp: serverTimestamp(),
uid: user.uid,
})*/
onValue(ref(database, `/rooms/${path}`), (roomData) => {
roomData = roomData.val();
setChatRoomObj(roomData)
if (!doneLoading) {
setDoneLoading(true)
}
})
}
}, [user]);
return (
<div>
{(authUser && doneLoading) && (
<div className="overflow-hidden h-dvh">
{/* Left Side of Page */}
<div className="overflow-hidden h-dvh md:mr-[400px]">
{/* Header */}
<Header
mainTab={"chat"}
chatRoomObj={chatRoomObj}
user={user}
sidebarControl={() => {setDrawerOpen(!drawerOpen)}}
/>
{/* Main Page Section */}
<div className="mr-2 h-[calc(100%-110px)]">
<ChatRoom roomObj={chatRoomObj} user={user} />
</div>
</div>
{/* Sidebar (Right Side of Page) */}
<Drawer open={drawerOpen} anchor={"right"} variant={windowSize.width > 767? "persistent": "temporary"} onClose={() => {setDrawerOpen(false)}} sx={{
width: windowSize.width > 767? 400: "80%",
marginTop: 10,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: windowSize.width > 767? 400: "80%",
borderLeft: 0,
},
}}>
<div className="shadow-2xl">
<Sidebar chatRoomObj={chatRoomObj}/>
</div>
</Drawer>
</div>
)}
</div>
);
}
export default Chat;
+17
View File
@@ -0,0 +1,17 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: DM",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
+139
View File
@@ -0,0 +1,139 @@
"use client";
// System Imports
import Drawer from '@mui/material/Drawer';
import { useState, useEffect } from "react";
// Firebase Imports
import { auth, database } from "../../../firebase-config";
import { ref, onValue, set, onDisconnect } from "firebase/database";
import { useAuthState } from "react-firebase-hooks/auth"
// Component Imports
import { Header } from "../../components/app/header";
import { DMRoom } from "../../components/app/friends/page";
import { Sidebar } from "../../components/app/sidebar/dm";
import {useWindowSize} from "../../components/app/datatypes";
/**
* DM Page
* @returns {Object} Chat Page
*/
function Chat() {
// State variables for chat page
const [user, setUser] = useState(null); // user data
const [chatRoomObj, setChatRoomObj] = useState(null); // Current chatroom object
const [doneLoading, setDoneLoading] = useState(false) // is the page done loading or not
const [authUser] = useAuthState(auth) // auth user object (used to obtain other user object)
const [drawerOpen, setDrawerOpen] = useState(true); // drawer open state
var windowSize = useWindowSize()
useEffect(() => {
if (windowSize.width < 767) {
setDrawerOpen(false)
} else {
setDrawerOpen(true)
}
}, [windowSize])
// Authentication Verification / Redirection if Profile Data not Filled out
useEffect(() => {
if (authUser) {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
setUser(userData);
} else {
window.location.href = "/onboarding";
}
});
}
}, [authUser])
// Users URL params to load proper chatroom, then logs the user into that room
useEffect(() => {
if (user) {
const searchParams = new URLSearchParams(document.location.search);
var path = searchParams.get("dm")
/*// Send entered message
var payload = {
body: "entered",
user: user.username,
isSystem: true,
timestamp: new Date().getTime(),
uid: user.uid,
};
set(
ref(
database,
`/rooms/${path}/chats/${new Date().getTime()}-${user.username}`
),
payload
);*/
// Add user to online for room
set(ref(database, `/dms/${path}/users/online/${user.uid}`), user)
// Removes user from room on disconnect (reload, window close, internet lost)
onDisconnect(ref(database, `/dms/${path}/users/online/${user.uid}`)).remove()
// Sends leaving message on disconnect (Timestamp function used due to new onDisconnect stuff)
/*someRef = ref(database, `/rooms/${path}/chats/${new Date().getTime()}-${user.username}`)
onDisconnect(someRef).set({
body: "left",
user: user.username,
isSystem: true,
timestamp: serverTimestamp(),
uid: user.uid,
})*/
onValue(ref(database, `/dms/${path}`), (roomData) => {
roomData = roomData.val();
setChatRoomObj(roomData)
if (!doneLoading) {
setDoneLoading(true)
}
})
}
}, [user]);
return (
<div>
{(authUser && doneLoading) && (
<div className="overflow-hidden h-dvh">
{/* Left Side of Page */}
<div className="overflow-hidden h-dvh md:mr-[400px]">
{/* Header */}
<Header
mainTab={"dm"}
chatRoomObj={chatRoomObj}
user={user}
sidebarControl={() => {setDrawerOpen(!drawerOpen)}}
/>
{/* Main Page Section */}
<div className="mr-2 h-[calc(100%-110px)]">
<DMRoom roomObj={chatRoomObj} user={user} />
</div>
</div>
{/* Sidebar (Right Side of Page) */}
<Drawer open={drawerOpen} anchor={"right"} variant={windowSize.width > 767? "persistent": "temporary"} onClose={() => {setDrawerOpen(false)}} sx={{
width: windowSize.width > 767? 400: "80%",
marginTop: 10,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: windowSize.width > 767? 400: "80%",
borderLeft: 0,
},
}}>
<div className="shadow-2xl">
<Sidebar chatRoomObj={chatRoomObj} user={user}/>
</div>
</Drawer>
</div>
)}
</div>
);
}
export default Chat;
+37
View File
@@ -0,0 +1,37 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background-color: aliceblue;
text-align: center;
text-wrap: pretty;
}
button {
background: #dee0e0;
border-color: black;
border: 5px;
border-radius: 5px;
padding: 5px;
margin: 5px;
filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15));
}
button:hover {
background-color: rgb(166, 166, 166);
}
input {
border: 1px solid black;
border-radius: 4px;
padding: 10px 10px;
margin: 5px;
}
input.err {
border: 2px solid red;
border-radius: 4px;
padding: 10px 10px;
margin: 5px;
}
+17
View File
@@ -0,0 +1,17 @@
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
+17
View File
@@ -0,0 +1,17 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Login",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
+135
View File
@@ -0,0 +1,135 @@
"use client";
// System Imports
import "../globals.css";
import { useForm, Form } from "react-hook-form";
import Link from "next/link"
import { useRouter } from "next/navigation";
// Firebase Imports
import { auth } from "../../../firebase-config";
import {setPersistence,signInWithEmailAndPassword,indexedDBLocalPersistence } from "firebase/auth";
/**
* Login Page
* @returns {Object} Login Page
*/
function Login() {
var router = useRouter();
var {register,control,setError,reset,formState: { errors, isSubmitting, isSubmitted },} = useForm();
/**
* Logs into Firebase authentication
* @param {JSON} data - User Login Data (data.email, data.password)
* @returns {void}
*/
function authenticate({data}) {
setPersistence(auth, indexedDBLocalPersistence).then(() => {
signInWithEmailAndPassword(auth, data.email, data.password).then(
(userCredential) => {
if (userCredential.user) {
router.push("/app");
}
}
).catch((error) => {
if (error = "auth/invalid-credential") {
const formError = {
type: "server",
message: "Username or Password Incorrect",
};
// set same error in both:
setError("password", formError);
setError("email", formError);
reset(data,{keepErrors: true})
} else {
const formError = {
type: "server",
message: "Server Error, Please try again later.",
};
// set same error in both:
setError("password", formError);
setError("email", formError);
reset(data,{keepErrors: true})
}
// ..
});
});
}
return (
<div>
<div className="grid h-screen place-items-center">
<div>
<Link href="/">
<img src="logos/logo_transparent_inverse.png" />
</Link>
<span className="text-[36px]">Chat with friends!</span>
<div>
<h3 className="text-[24px] mt-[25px] mb-2">Login</h3>
{errors.email && errors.password && (
<div className="text-[red] mb-2 text-[18px] text-bold">
Invalid Email or Password.
</div>
)}
<Form
onSubmit={authenticate}
encType={"application/json"}
control={control}
>
<input
type="email"
id="email"
className={errors.email && errors.password && "err"}
{...register("email", { required: true })}
placeholder="Enter Email Address"
/>
<br />
<input
type="password"
id="password"
name="password"
className={errors.email && errors.password && "err"}
{...register("password", { required: true })}
placeholder="Enter Password"
/>
<br />
<button
type="submit"
className="inline-flex items-center transition ease-in-out duration-150 m-5 bg-cyan-500 text-white font-bold py-2 px-4 rounded-full"
>
{(isSubmitting || isSubmitted) && (
<span className="inline-block">
<svg
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
strokeWidth="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</span>
)}
Log In
</button>
<br />
Need an account? <Link href="/register">Sign Up</Link>
<br />
</Form>
</div>
</div>
</div>
</div>
);
}
export default Login;
@@ -0,0 +1,17 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Onboarding",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
+87
View File
@@ -0,0 +1,87 @@
"use client";
// System Imports
import "../globals.css";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
// Firebase Imports
import { ref, set } from "firebase/database";
import { auth, database } from "../../../firebase-config";
import { onAuthStateChanged } from "firebase/auth";
/**
* Creates user data in Firebase DB
* @param {JSON} data - User data to be stored in Firebase DB ( from form )
* @return {Boolean} - True if user data is stored, False if user data is not stored
*/
function createUser(data) {
onAuthStateChanged(auth, (user) => {
if (user.uid) {
data.uid = user.uid;
data.defined = true;
data.email = user.email;
set(ref(database, `users/${user.uid}`), data);
return true;
} else {
return false;
}
});
}
/**
* Onboarding Page
* @returns {Object} - Onboarding Page
*/
function Onboarding() {
var router = useRouter();
var { register, handleSubmit } = useForm();
function Onboard(data) {
createUser(data);
router.push("/app");
}
return (
<div>
<div className="grid h-screen place-items-center">
<div>
<img src="logos/logo_transparent_inverse.png" />
<span className="text-[36px]">Chat with friends!</span>
<div className="m-5">
Welcome to ChatMaps! We are excited to have you join our community!
<br />
First we just need a little bit of information from you to get
started.
</div>
<form action="#" onSubmit={handleSubmit(Onboard)}>
<input
type="text"
{...register("username")}
placeholder="Display Name"
/>
<br />
<input
type="text"
{...register("firstName")}
placeholder="First Name"
/>
<br />
<input
type="text"
{...register("lastName")}
placeholder="Last Name"
/>
<br />
<button
type="submit"
className="inline-flex items-center transition ease-in-out duration-150 m-5 bg-cyan-500 text-white font-bold py-2 px-4 rounded-full"
>
Save
</button>
</form>
</div>
</div>
</div>
);
}
export default Onboarding;
+111
View File
@@ -0,0 +1,111 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link"
import { auth, database } from "../../firebase-config";
import { ref, get } from "firebase/database";
import { onAuthStateChanged } from "firebase/auth";
// Capacitor Import
import { Geolocation } from '@capacitor/geolocation';
/**
* Home Page
* @returns {Object} - Home Page
*/
function Home() {
const [isLoadingLoc, setLoadingLoc] = useState(true); // is location loading?
const [roomCount, setRoomCount] = useState(null); // local room count
const [isAuthenticated, setAuth] = useState(false); // is user authenticated?
const [coords, setCoords] = useState(null)
// Authentication
useEffect(() => {
onAuthStateChanged(auth, async (user) => {
if (user) {
setAuth(true);
} else {
setAuth(false);
}
});
}, []);
useEffect(() => {
Geolocation.getCurrentPosition().then((position) => {
setCoords(position.coords);
setLoadingLoc(false);
});
}, [])
// Update room count on location fix
useEffect(() => {
if (coords) {
var path =
String(coords.latitude.toFixed(2)).replace(".", "") +
"/" +
String(coords.longitude.toFixed(2)).replace(".", "");
get(ref(database, `/rooms/${path}`)).then((snapshot) => {
if (snapshot.exists()) {
var count = 0;
for (var room in snapshot.val()) {
count += 1;
}
setRoomCount(count);
} else {
setRoomCount(0);
}
setLoadingLoc(false);
});
}
}, [coords])
return (
<div>
<div className="grid h-screen place-items-center">
<div>
<img src="logos/logo_transparent_inverse.png" />
<span className="text-[36px]">Chat with friends!</span>
<div className="m-5">
{!isAuthenticated && (
<div>
<Link href="/login">
<button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">
Login
</button>
</Link>
<Link href="/register">
<button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">
Register
</button>
</Link>
</div>
)}
{isAuthenticated && (
<Link href="/app">
<button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">
Continue to App
</button>
</Link>
)}
{!isLoadingLoc && roomCount == 1 && (
<div className="text-[24px] pt-10">
Join others in {roomCount} room near you!
</div>
)}
{!isLoadingLoc && roomCount != 1 && roomCount != 0 && (
<div className="text-[24px] pt-10">
Join others in {roomCount} rooms near you!
</div>
)}
{(isLoadingLoc || (roomCount == 0 && !isLoadingLoc)) && (
<div className="text-[24px] pt-10">
Start the conversation today!
</div>
)}
</div>
</div>
</div>
</div>
);
}
export default Home;
+17
View File
@@ -0,0 +1,17 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Register",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
+147
View File
@@ -0,0 +1,147 @@
"use client";
// System Imports
import "../globals.css";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useForm, Form } from "react-hook-form";
import Link from "next/link"
// Firebase Imports
import {createUserWithEmailAndPassword,signInWithEmailAndPassword,setPersistence,indexedDBLocalPersistence,} from "firebase/auth";
import { auth } from "../../../firebase-config";
/**
* Signs up user in Firebase Authentication
* @param {JSON} data - User signup data (data.email, data.password)
* @returns {Boolean} - True if user is signed up, False if user is not signed up
* @async
*/
async function Signup(data) {
var userCredential = await createUserWithEmailAndPassword(
auth,
data.email,
data.password
);
if (userCredential.user) {
setPersistence(auth, indexedDBLocalPersistence).then(() => {
signInWithEmailAndPassword(auth, data.email, data.password).then(
() => {
return true;
}
);
});
} else {
return false;
}
}
/**
* Register Page
* @returns {Object} - Registration Page
*/
function Register() {
var router = useRouter();
var {
register,
control,
formState: { errors },
} = useForm();
var emailRegex =
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
var [passwordMismatch, setPasswordMismatch] = useState(false);
var [passwordLength, setPasswordLength] = useState(false);
const passwordMatch = (data) => {
return data.password === data.passwordCheck;
};
const passwordLengthMatch = (data) => {
return data.password.length >= 6;
}
/**
* Form onSubmit Handler
* @params {JSON} data - Form data
*/
function onSubmit({ data }) {
if (passwordMatch(data) && passwordLengthMatch(data)) {
setPasswordMismatch(false);
setPasswordLength(false)
if (Signup(data)) {
router.push("/onboarding");
}
} else {
if (!passwordMatch(data)) {
setPasswordMismatch(true);
}
if (!passwordLengthMatch(data)) {
setPasswordLength(true);
}
return;
}
}
return (
<div>
<div className="grid h-screen place-items-center">
<div>
<Link href="/">
<img src="logos/logo_transparent_inverse.png" />
</Link>
<span className="text-[36px]">Chat with friends!</span>
<div>
<h3 className="text-[24px] mt-[15px]">Register</h3>
<Form
onSubmit={onSubmit}
encType={"application/json"}
control={control}
>
<input
type="email"
{...register("email", { required: true, pattern: emailRegex })}
className={errors.email && "err"}
placeholder="Enter Email Address"
/>
<br />
<input
type="password"
{...register("password", { required: true })}
className={
errors.password && errors.password.type == "required" && "err"
}
placeholder="Enter Password"
/>
<br />
<input
type="password"
{...register("passwordCheck", { required: false })}
className={
errors.passwordCheck &&
errors.passwordCheck.type == "required" &&
"err"
}
placeholder="Re-enter Password"
/>
<br />
{passwordMismatch && (
<p className="text-red-500">Passwords do not match</p>
)}
{passwordLength && (
<p className="text-red-500">Password must be at least 6 characters</p>
)}
<button
type="submit"
className=" m-5 bg-cyan-500 text-white font-bold py-2 px-4 rounded-full"
>
{" "}
Register
</button>
<br />
Have an account? <Link href="/login">Login</Link>
</Form>
</div>
</div>
</div>
</div>
);
}
export default Register;
+17
View File
@@ -0,0 +1,17 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: User Profile",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
+177
View File
@@ -0,0 +1,177 @@
"use client";
// System Imports
import { useState, useEffect } from "react";
import { auth, database } from "../../../firebase-config";
import { ref, onValue, get } from "firebase/database";
import { onAuthStateChanged } from "firebase/auth";
// Refactored Component Imports
// Data Structure Imports
import { ProfileRoom } from "../../components/app/profile/ProfileRoom";
import { ProfileEdit } from "../../components/app/profile/ProfileEdit";
import { Interest } from "../../components/app/profile/Interest";
// Header Import
import { Header } from "../../components/app/header";
// Friend Import
import { addFriend } from "../../components/app/friends/friends";
/**
* User Profile Page
* @returns {Object} - User Profile Page
*/
function UserProfile() {
const [profileData, setProfileData] = useState(null); // Profile Data
const [isAuthenticated, setIsAuthenticated] = useState(false); // Determines if user is authenticated
const [user, setUser] = useState(null); // User Data
const [userInterestArray, setUserInterestArray] = useState(null); // Array of user's interests
const [userRoomsArray, setUserRoomsArray] = useState(null); // Array of user's rooms
const [isOwner, setIsOwner] = useState(false); // Determines if user is owner of profile
const [friends, setFriends] = useState(false); // is user a friend?
const [isPending, setPending] = useState(false); // is friend request pending?
// Handles Edit State in Component, shares useState with ProfileEdit
const [isEditing, setIsEditing] = useState(false);
const handleIsEditing = (newValue) => {
setIsEditing(newValue);
};
// Authentication
useEffect(() => {
onAuthStateChanged(auth, (user) => {
const searchParams = new URLSearchParams(document.location.search);
var userUID = searchParams.get("uid")
if (user) {
get(ref(database, `users/${user.uid}`)).then((userData) => {
userData = userData.val();
if (userData) {
if (userData.uid == userUID) {
setIsOwner(true);
}
setUser(userData);
setIsAuthenticated(true);
} else {
window.location.href = "/onboarding";
}
});
}
});
}, []);
// Grabs profile user data
useEffect(() => {
const searchParams = new URLSearchParams(document.location.search);
var userUID = searchParams.get("uid")
onValue(ref(database, "/users/" + userUID), (snapshot) => {
setProfileData(snapshot.val());
// Populates array with user's interests
var interests = snapshot.val().interests || null;
if (interests == null) {
// Placeholder for no interests specified, will be replaced with default interests
interests = "Music, Sports, Movies";
}
interests = interests.split(",");
var interestArray = [];
var i = 0;
for (var interest in interests) {
if (i < 4)
interestArray.push(<Interest interest={interests[interest]} />);
i++;
}
setUserInterestArray(interestArray);
// Populates array with user's rooms
var rooms = snapshot.val().rooms;
var roomArray = [];
for (var room in rooms) {
roomArray.push(<ProfileRoom room={rooms[room]} />);
}
setUserRoomsArray(roomArray);
});
}, []);
useEffect(() => {
if (user && profileData) {
if ("friends" in user) {
profileData.uid in user.friends.friends ? setFriends(true) : setFriends(false);
}
if ("friends" in profileData) {
if ("requests" in profileData.friends) {
user.uid in profileData.friends.requests ? setPending(true) : setPending(false);
}
}
}
}, [user, profileData]);
return (
<div>
{isAuthenticated && (
<div className="md:overflow-hidden">
{/* Left Side of Page */}
<div className="h-dvh md:overflow-hidden">
{/* Header */}
<Header user={user} />
{/* Main Page Section */}
<div className="md:grid md:grid-cols-3 mr-2 h-[calc(100%-110px)] pl-5 pr-5 pt-2 max-md:mb-10">
<div className="cols-span-1 bg-white shadow-2xl rounded-xl pt-5 max-md:pb-5">
{!isEditing && (
<div>
<img
src={profileData.pfp}
width="300px"
className="relative mx-auto rounded-2xl overflow-hidden"
/>
<div className="font-bold text-[30px]">
{profileData.firstName} {profileData.lastName}
</div>
<div className="text-[20px]">@{profileData.username}</div>
<div className="pt-5">{profileData.bio}</div>
<div className="grid grid-cols-3 p-3">
{userInterestArray}
</div>
<div className="grid grid-cols-1 auto-cols-min justify-items-center">
{isOwner && (
<a
onClick={() => {
setIsEditing(true);
}}
className="w-[120px] p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full text-center"
>
Edit Profile
</a>
)}
{(!isOwner && !friends) && (
<div>
{(!isPending ) && (<a onClick={() => {addFriend(user, profileData.uid);setPending(true)}} className="w-[120px] p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full text-center">Add Friend</a>)}
{(isPending ) && (<a className="w-[120px] p-2 bg-cyan-500 text-white font-bold rounded-full text-center">Pending</a>)}
</div>
)}
{(!isOwner && friends ) && (<div className="font-bold text-[20px]">Friends</div>)}
</div>
</div>
)}
{isEditing && (
<ProfileEdit
profileData={profileData}
user={user}
onSave={handleIsEditing}
/>
)}
</div>
<div className="col-span-2">
<div className="h-dvh pb-20 overflow-auto grid md:grid-cols-3 max-md:grid-cols-1 max-md:mt-5 md:pl-5 justify-items-center gap-y-5 gap-1 w-[100%]">
{userRoomsArray}
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
}
export default UserProfile;
@@ -0,0 +1,219 @@
import Link from "next/link"
import { useEffect, useState } from "react";
// Icons
import PersonIcon from '@mui/icons-material/Person';
// Colors for Messages
const userColors = [
"#ff80ed",
"#065535",
"#133337",
"#ffc0cb",
"#e6e6fa",
"#ffd700",
"#ffa500",
"#0000ff",
"#1b85b8",
"#5a5255",
"#559e83",
"#ae5a41",
"#c3cb71",
];
let dateOptions = {
weekday: "long",
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
};
/**
* Rich Message Formatting
* @param {String} message - Message to Format
* @returns {String} - Formatted Message (IN HTML)
*/
function RMF(message) {
var URLREGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
var URLmatch = message.match(URLREGEX);
if (URLmatch) {
for (var i = 0; i < URLmatch.length; i++) {
var link = (<span>
{message.split(URLmatch[i])[0]}
<Link href={"https://"+URLmatch[i]} target="_blank" className="hover:underline">{URLmatch[i]}</Link>
{message.split(URLmatch[i])[1]}
</span>)
message = link
}
}
return message
}
/**
* Grabs Window Size
* @returns {Object} - Window Size Object (width, height)
*/
export function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener("resize", handleResize);
handleResize();
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowSize;
}
/**
* Generates Color based on string hash
* @param {String} user_str - Username / String for hashing
* @returns {String} - Color Hex Code Index in userColors
*/
const generateColor = (user_str) => {
// hashes username for consistent colors, maybe all functionality to pick color later
let hash = 0;
for (let i = 0; i < user_str.length; i++) {
hash = user_str.charCodeAt(i) + (hash * 32 - hash);
}
const index = Math.abs(hash) % userColors.length;
return index;
};
/**
* Chat Message Object
* @props {JSON} chatObj - Chat Object
* @returns {Object} - Chat Message Component
*/
export function Chat({ chatObj }) {
var message = RMF(chatObj.body)
return (
<div className="width-[100%] bg-white rounded-lg mt-1 text-left p-1 grid grid-cols-2 mr-2">
<div>
<span style={{ color: userColors[generateColor(chatObj.user)] }}>
<Link href={`/user?uid=${chatObj.uid}`}
className="hover:font-bold cursor-pointer">
{chatObj.user}
</Link>
</span>
: {message}
</div>
<div className="text-right text-[#d1d1d1]">
{new Date(chatObj.timestamp).toLocaleString(dateOptions)}
</div>
</div>
);
}
/**
* System Chat Message Object
* @prop {JSON} chatObj - Chat Object
* @returns {Object} - System Chat Message Component
*/
export function SystemMessage({ chatObj }) {
return (
<div className="width-[100%] bg-white rounded-lg mt-1 text-left p-1 grid grid-cols-2 mr-2">
<div className="text-[#d1d1d1]">
<span style={{ color: userColors[generateColor(chatObj.user)] }}>
<Link href={`/user?uid=${chatObj.uid}`}
className="hover:font-bold cursor-pointer">
{chatObj.user}
</Link>
</span>{" "}
has {chatObj.body} the room.
</div>
<div className="text-right text-[#d1d1d1]">
{new Date(chatObj.timestamp).toLocaleString(dateOptions)}
</div>
</div>
);
}
/**
* Member Object for Sidebar
* @prop {JSON} memberObj - Member Object
* @returns {Object} - Member Component
*/
export function Member({ memberObj }) {
return (
<Link href={"/user?uid=" + memberObj.uid}>
<div className="cursor-pointer g-[aliceblue] rounded-lg m-3 shadow-xl p-2">
{memberObj.username}
</div>
</Link>
);
}
/**
* Chat Room Object for Sidebar
* @prop {JSON} roomObj - Room Object
* @returns {Object} - Chat Room Component
*/
export function ChatRoomSidebar({ roomObj }) {
const [isRoomHovered, setIsRoomHovered] = useState(false);
if ("users" in roomObj) {
if ("online" in roomObj.users) {
var roomOnline = Object.keys(roomObj.users.online).length
} else {
var roomOnline = 0
}
if ("all" in roomObj.users) {
var roomTotal = Object.keys(roomObj.users.all).length
} else {
var roomTotal = 0
}
} else {
var roomOnline = 0
var roomTotal = 0
}
const handleMouseEnter = () => {
setIsRoomHovered(true);
};
const handleMouseLeave = () => {
setIsRoomHovered(false);
};
return (
<div className="border-[black] border-1 shadow-lg p-2 m-2 rounded-lg cursor-pointer">
<Link href={`/chat?room=${roomObj.path}/${roomObj.name}-${roomObj.timestamp}`}>
<div className="grid grid-cols-3">
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<PersonIcon/>
{isRoomHovered && roomObj.users && (
<div
className="fixed bg-white p-2 shadow-md"
>
<ul>
{Object.values(roomObj.users.all).map((user, index) => ( // I hate making lists like this
<li key={index}>{user.username}</li>
))}
</ul>
</div>
)}
<div>{roomOnline} / {roomTotal}</div>
</div>
<div className="col-span-2">
<div className="font-bold">{roomObj.name}</div>
<div className="italic">{roomObj.description}</div>
</div>
</div>
</Link>
</div>
);
}
// This will be removed once dateOptions is no longer used in this file
export { dateOptions };
@@ -0,0 +1,57 @@
// Firebase Imports
import { database } from "../../../../firebase-config"
import { ref, set, get } from "firebase/database";
import ChatIcon from '@mui/icons-material/Chat';
export function openDM(user, uid) {
var uid1 = user.uid < uid? user.uid : uid
var uid2 = user.uid > uid? user.uid : uid
get(ref(database, `dms/${uid1}-${uid2}`)).then((snapshot) => {
if (snapshot.exists()) {
window.location.href = `/dm?dm=${uid1}-${uid2}`
} else {
createDM(user, uid)
window.location.href = `/dm?dm=${uid1}-${uid2}`
}
});
}
export function createDM(user, uid) {
var uid1 = user.uid < uid? user.uid : uid
var uid2 = user.uid > uid? user.uid : uid
set(ref(database, `dms/${uid1}-${uid2}`), {
initUID: user.uid,
targetUID: uid,
room: uid1 + '-' + uid2,
UIDs: [user.uid, uid]
})
}
/**
*
* @param {JSON} friendObj - Friend Object (user)
* @returns DM Component
*/
export function DM({user,friendObj}) {
return (
<div className="border-[black] border-1 shadow-lg m-2 rounded-lg">
<div className='grid grid-cols-4'>
<div className='place-content-center'>
<ChatIcon className='cursor-pointer' onClick={() => {openDM(user,friendObj.uid)}}/>
</div>
<div className='col-span-3 cursor-pointer'>
<div onClick={() => {openDM(user,friendObj.uid)}}>
<div className='inline-block mr-5'><img src={friendObj.pfp} className='w-[50px]'/></div>
<div className='inline-block relative top-[-6px]'>
<div className="font-bold">{friendObj.firstName} {friendObj.lastName}</div>
<div className="">@{friendObj.username}</div>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,104 @@
import Link from 'next/link'
// Icons
import ChatIcon from '@mui/icons-material/Chat';
// Firebase Imports
import { database } from "../../../../firebase-config"
import { ref, set } from "firebase/database";
import { openDM } from './dm';
// Icons
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
/**
* Send a friend request to a user
* @param {JSON} user - User Object
* @param {JSON} uid - User ID of the user to send a friend request to
*/
export function addFriend(user, uid) {
// Add to user's friend requests
set(ref(database, `users/${uid}/friends/requests/${user.uid}`), {
uid: user.uid
})
}
/**
*
* @param {JSON} friendObj - Friend Object (user)
* @returns Friend Component
*/
export function Friend({user,friendObj}) {
return (
<div className="border-[black] border-1 shadow-lg m-2 rounded-lg">
<div className='grid grid-cols-4'>
<div className='place-content-center'>
<ChatIcon className='cursor-pointer' onClick={() => {openDM(user,friendObj.uid)}}/>
</div>
<div className='col-span-3 cursor-pointer'>
<Link href={`/user?uid=${friendObj.uid}`}>
<div className='inline-block mr-5'><img src={friendObj.pfp} className='w-[50px]'/></div>
<div className='inline-block relative top-[-6px]'>
<div className="font-bold">{friendObj.firstName} {friendObj.lastName}</div>
<div className="">@{friendObj.username}</div>
</div>
</Link>
</div>
</div>
</div>
)
}
/**
*
* @prop {JSON} user - User Object
* @prop {JSON} requestingUser - User Object of the user requesting to be friends
* @returns
*/
export function FriendRequest({user, requestingUser}) {
/**
* Accepts Friend Request
* @param {JSON} user - User Object
* @param {JSON} uid - User ID of the user who sent the friend request
*/
function acceptRequest(user, uid) {
// Add to user's friends
set(ref(database, `users/${user.uid}/friends/friends/${uid}`), {
uid: uid
})
set(ref(database, `users/${uid}/friends/friends/${user.uid}`), {
uid: user.uid
})
removeRequest(user, uid)
}
/**
* Removes Friend Request
* @param {JSON} user - User Object
* @param {JSON} uid - User ID of the user who sent the friend request
*/
function removeRequest(user, uid) {
// Remove from user's friend requests
set(ref(database, `users/${user.uid}/friends/requests/${uid}`), null)
}
return (
<div className="border-[black] border-1 shadow-lg m-2 rounded-lg">
<div className='grid grid-cols-4'>
<div className='place-content-center'>
<CheckIcon className='cursor-pointer ml-5' onClick={() => {acceptRequest(user, requestingUser.uid)}}/>
<CloseIcon className='cursor-pointer ml-5' onClick={() => {removeRequest(user, requestingUser.uid)}}/>
</div>
<div className='col-span-3 cursor-pointer'>
<Link href={`/user?uid=${requestingUser.uid}`}>
<div className='inline-block mr-5'><img src={requestingUser.pfp} className='w-[50px]'/></div>
<div className='inline-block relative top-[-6px]'>
<div className="font-bold">{requestingUser.firstName} {requestingUser.lastName}</div>
<div className="">@{requestingUser.username}</div>
</div>
</Link>
</div>
</div>
</div>
)
}
@@ -0,0 +1,87 @@
// Dependency Imports
import { Form, useForm } from "react-hook-form";
// Firebase Imports
import { ref, set } from "firebase/database";
import { database } from "../../../../firebase-config";
// Component Imports
import { Chat, SystemMessage } from "../datatypes";
// Icons
import SendIcon from '@mui/icons-material/Send';
/**
* Chat Room Component
* @prop {JSON} roomObj - Room Object
* @prop {JSON} user - User Object
* @returns {Object} - Chat Room Component
*/
export function DMRoom({ roomObj, user }) {
var { register, control, reset, handleSubmit } = useForm();
// Message updater
var chatsArr = [];
var messages = roomObj.chats;
for (var message in messages) {
if (messages[message].isSystem) {
chatsArr.push(
<SystemMessage
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
} else {
chatsArr.push(
<Chat
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
}
}
var chats = chatsArr.reverse();
/**
* Send Message in Chatroom
* @param {JSON} data - Message data to send (from form)
* @returns {void}
*/
function sendMessage(data) {
reset();
var payload = {
body: data.message,
user: user.username,
uid: user.uid,
isSystem: false,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/dms/${roomObj.room}/chats/${new Date().getTime()}-${user.username}`
),
payload
);
}
if (!chats) return <div>No Chats</div>;
return (
<div className="m-1 h-[100%] rounded-lg">
<div className="h-[90%] m-4 overflow-y-auto flex flex-col-reverse">
{chats}
</div>
<div className="m-2 h-[10%] w-[100%] bg-white rounded-lg">
<Form
onSubmit={handleSubmit(sendMessage)}
control={control}
>
<div className="width-[100%] grid grid-cols-6 pr-5 pt-1">
<input type="text" {...register("message")} placeholder="Enter message..." className="col-span-5 border-[0px]" />
<button className="p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full mr-5 w-[100%]"><SendIcon/></button>
</div>
</Form>
</div>
</div>
);
}
+148
View File
@@ -0,0 +1,148 @@
// System Imports
import Link from "next/link"
// Firebase Imports
import { database } from "../../../firebase-config";
import { ref, set, remove } from "firebase/database";
// Component Imports
import { NotificationPanel } from "./notifications/notifications";
import { ProfilePanel } from "./profile/ProfilePanel"
// Icons
import MenuIcon from '@mui/icons-material/Menu';
import AddIcon from '@mui/icons-material/Add';
import RemoveIcon from '@mui/icons-material/Remove';
import CloseIcon from '@mui/icons-material/Close';
/**
* Closes Chat
* @param {JSON} chatRoomObj - Chat Room Object
* @param {JSON} user - User Object
* @returns {void}
*/
function closeChat(chatRoomObj, user) {
remove(ref(database, `/rooms/${chatRoomObj.path}/${chatRoomObj.name}-${chatRoomObj.timestamp}/users/online/${user.uid}`))
}
/**
* Adds Chat Room to My Rooms
* @param {JSON} chatRoomObj - Chat Room Object
* @param {JSON} user - User Object
* @returns {void}
*/
function addToMyRooms(chatRoomObj, user) {
set(
ref(
database,
`/users/${user.uid}/rooms/${chatRoomObj.name}-${chatRoomObj.timestamp}`
),
{
name: chatRoomObj.name,
path: chatRoomObj.path,
timestamp: chatRoomObj.timestamp,
description: chatRoomObj.description,
longitude: chatRoomObj.longitude,
latitude: chatRoomObj.latitude,
}
);
var path =
chatRoomObj.path + "/" + chatRoomObj.name + "-" + chatRoomObj.timestamp;
set(ref(database, `/rooms/${path}/users/all/${user.uid}`), user);
}
/**
* Removes Chat Room from My Rooms
* @param {JSON} chatRoomObj - Chat Room Object
* @param {JSON} user - User Object
* @returns {void}
*/
function removeFromMyRooms(chatRoomObj, user) {
var path =
chatRoomObj.path + "/" + chatRoomObj.name + "-" + chatRoomObj.timestamp;
remove(
ref(
database,
`/users/${user.uid}/rooms/${chatRoomObj.name}-${chatRoomObj.timestamp}`
)
);
remove(ref(database, `/rooms/${path}/users/all/${user.uid}`));
}
/**
* Header Component
* @prop {String} mainTab - Main Tab
* @prop {JSON} chatRoomObj - Chat Room Object
* @prop {JSON} user - User Object
*/
export function Header({mainTab,chatRoomObj,user,sidebarControl}) {
if (mainTab == "chat") {
var roomName = chatRoomObj.name + "-" + chatRoomObj.timestamp;
if (user.rooms != null && roomName in user.rooms) {
// its in there
var isMyRoom = true;
} else {
// its not in there
var isMyRoom = false;
}
}
return (
<div className="flex m-2 rounded-lg h-[63px] bg-white shadow-2xl p-1">
<div className="flex shrink h-[60px]">
<Link href="/app">
<img src="/logos/logo_transparent_inverse.png" className="h-[60px] max-xl:hidden" />
<img src="/logos/icon.png" className="h-[50px] mt-[5px] mb-[5px] xl:hidden" />
</Link>
</div>
<div className="grow grid grid-rows-1 grid-flow-col auto-cols-max justify-end gap-2 h-[60px] p-2">
{mainTab == "chat" && isMyRoom == false && (
<a
onClick={() => {
addToMyRooms(chatRoomObj, user);
}}
className="p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full mr-2 flex items-center"
>
<AddIcon/>
</a>
)}
{mainTab == "chat" && isMyRoom == true && (
<a
onClick={() => {
removeFromMyRooms(chatRoomObj, user);
}}
className="p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full mr-2 flex items-center"
>
<RemoveIcon/>
</a>
)}
{(mainTab == "chat" || mainTab == "dm" ) && (
<Link
href="/app"
className="p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full mr-2 flex items-center"
onClick={() => {closeChat(chatRoomObj,user)}}
>
<CloseIcon/>
</Link>
)}
{/* Notifications Panel */}
<NotificationPanel user={user}/>
{/*Profile Dropdown */}
<ProfilePanel user={user}/>
{/* Sidebar Control (for small screens) */}
<div
className="md:hidden p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full mr-5 flex items-center"
onClick={() => {sidebarControl()}}
>
<MenuIcon/>
</div>
</div>
</div>
);
}
@@ -0,0 +1,85 @@
import { Map, Marker, ZoomControl } from "pigeon-maps";
import { database } from "../../../../firebase-config";
import { ref, get } from "firebase/database";
import { useState, useEffect } from "react";
// ONLY nearby markers
function NearbyRoomMarkers({ loc, user }) {
const [markerArr, setMarkerArr] = useState([]);
// Room path in DB
const path =
String(loc.latitude.toFixed(2)).replace(".", "") +
"/" +
String(loc.longitude.toFixed(2)).replace(".", "") +
"/";
// Sorry for the href but <Link> doesn't work here
const handleRoomMarkerClick = (roomObj) => {
window.location.href =
"/chat?room=" + path + roomObj.name + "-" + roomObj.timestamp;
};
// Mostly copied Nick's code from before
useEffect(() => {
if (loc && user) {
get(ref(database, `/rooms/${path}`)).then((snapshot) => {
if (snapshot.exists()) {
const rooms = snapshot.val();
const newMarkers = Object.values(rooms).map((roomObj, index) => {
const markerKey = roomObj.path + "-" + index;
return (
// Want to change this to be something other than markers (or something extra)
<Marker
key={markerKey}
anchor={[roomObj.latitude, roomObj.longitude]}
color="blue"
onClick={() => handleRoomMarkerClick(roomObj)}
></Marker>
);
});
setMarkerArr(newMarkers);
}
});
}
}, []);
return markerArr;
}
/**
* Geo Component for Wrapping Map
* @constructor
* @prop {JSON} loc - Location Object {latitude, longitude}
* @prop {Number} zoom - Zoom Level
* @prop {Boolean} moveable - Moveable Map
* @prop {Boolean} markers - Enable Markers
* @returns {Map} - Geo Component (As Map)
*/
export function Geo({ loc, zoom, moveable, markers, user }) {
const handleUserMarkerClick = () => {
window.location.href = "/user?uid=" + user.uid;
}
if (!loc) {
return <div>Getting Location...</div>;
} else {
return (
<>
<Map
center={[loc.latitude, loc.longitude]}
defaultZoom={zoom}
mouseEvents={moveable}
touchEvents={moveable}
>
{zoom && <ZoomControl />}
{markers && NearbyRoomMarkers({ loc, user })}
{user && ( // Shows the user marker
<Marker anchor={[loc.latitude, loc.longitude]} color="red" onClick={handleUserMarkerClick} />
)}
</Map>
</>
);
}
}
@@ -0,0 +1,101 @@
// System Imports
import { Popover } from "@headlessui/react";
// Icon Imports
import NotificationsIcon from '@mui/icons-material/Notifications';
import NotificationsPausedIcon from '@mui/icons-material/NotificationsPaused';
import CloseIcon from '@mui/icons-material/Close';
// Firebase Imports
import { database } from "../../../../firebase-config";
import { ref, set, remove } from "firebase/database";
/**
* Notification Object
* @constructor
* @prop {user.notification} data - Notification data
* @returns {Notification} - Notification Component
*/
function Notification({data}) {
return (
<div className="hover:bg-[#C0C0C0] rounded-lg">
<div className="float-right top-0 cursor-pointer p-2 text-[24px] text-slate-500">
<div onClick={() => {removeNotification(data.id)}}><CloseIcon/></div>
</div>
<div className="p-3 text-left">
{data.title}<br/>
{data.byline}<br/>
</div>
</div>
)
}
/**
* Removes Notification
* @param {String} ruser - Receiving user UID (User Whose Notifications are being removed)
* @param {String} dataID - Notification ID
* @returns {void}
*/
function removeNotification(ruser, dataID) {
remove(ref(database, `/user/${ruser}/notifications/${dataID}`))
}
/**
* Creates New Notification
* @param {String} title - Title of the notification
* @param {String} byline - Byline of the notification
* @param {String} action - Action to perform (friend request [fr], more to come)
* @param {String} suser - Sending user UID
* @param {String} ruser - Receiving user UID
* @returns {void}
*/
function createNotification(title, byline, action, suser, ruser) {
var timestamp = new Date().getTime();
var payload = {
title: title,
byline: byline,
action: action,
suser: suser,
ruser: ruser
};
set(ref(database, `/user/${ruser}/notifications/${timestamp}-${suser}`), payload);
}
/**
* Notification Panel
* @constructor
* @prop {user} user - User object (from Firebase)
* @returns {NotificationPanel} - Notification Panel Component
*/
export function NotificationPanel({user}) {
var notificationsMap = []
if (user.notification) {
for (var notificationPackage in user.notification) {
notificationsMap.push(<Notification data={user.notification[notificationPackage]}/>)
}
var isNotifications = true
} else {
var isNotifications = false
}
return (
<Popover className="relative">
<Popover.Button as="div">
<div className="h-[44px] p-[8px] cursor-pointer bg-cyan-500 text-white font-bold rounded-full shadow-2xl flex items-center">
<NotificationsIcon />
</div>
</Popover.Button>
<Popover.Panel className="absolute z-10 bg-white mt-[4px] rounded-xl ml-3 shadow-2xl w-64 md:right-[0px] max-md:right-[-300%]">
<div className="grid grid-cols-1">
{isNotifications && notificationsMap}
{!isNotifications &&
<div className="h-[64px] flex flex-col justify-center items-center">
<NotificationsPausedIcon/> All caught up!
</div>
}
</div>
</Popover.Panel>
</Popover>
)
}
@@ -0,0 +1,89 @@
// Dependency Imports
import { Form, useForm } from "react-hook-form";
// Firebase Imports
import { ref, set } from "firebase/database";
import { database } from "../../../../firebase-config";
// Component Imports
import { Chat, SystemMessage } from "../datatypes";
// Icons
import SendIcon from '@mui/icons-material/Send';
/**
* Chat Room Component
* @prop {JSON} roomObj - Room Object
* @prop {JSON} user - User Object
* @returns {Object} - Chat Room Component
*/
export function ChatRoom({ roomObj, user }) {
var { register, control, reset, handleSubmit } = useForm();
// Message updater
var chatsArr = [];
var messages = roomObj.chats;
for (var message in messages) {
if (messages[message].isSystem) {
chatsArr.push(
<SystemMessage
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
} else {
chatsArr.push(
<Chat
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
}
}
var chats = chatsArr.reverse();
/**
* Send Message in Chatroom
* @param {JSON} data - Message data to send (from form)
* @returns {void}
*/
function sendMessage(data) {
reset();
var payload = {
body: data.message,
user: user.username,
uid: user.uid,
isSystem: false,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/rooms/${
roomObj.path + "/" + roomObj.name + "-" + roomObj.timestamp
}/chats/${new Date().getTime()}-${user.username}`
),
payload
);
}
if (!chats) return <div>No Chats</div>;
return (
<div className="m-1 h-[100%] rounded-lg">
<div className="h-[90%] m-4 overflow-y-auto flex flex-col-reverse">
{chats}
</div>
<div className="m-2 h-[10%] w-[100%] bg-white rounded-lg">
<Form
onSubmit={handleSubmit(sendMessage)}
control={control}
>
<div className="width-[100%] grid grid-cols-6 pr-5 pt-1">
<input type="text" {...register("message")} placeholder="Enter message..." className="col-span-5 border-[0px]" />
<button className="p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full mr-5 w-[100%]"><SendIcon/></button>
</div>
</Form>
</div>
</div>
);
}
@@ -0,0 +1,41 @@
import { Geo } from "../map/geo";
/**
* Module for Welcome Message on main tab landing page
* @prop {JSON} user - User Object
* @returns {Object} - Welcome Message Component
*/
function WelcomeMessage({ user }) {
return (
<div className="bg-white rounded-lg m-2 mt-4 text-left p-2 pl-5">
<div>
Welcome, {user.firstName} {user.lastName} ({user.username})
</div>
<div>Lets see what&apos;s happening in your area.</div>
</div>
);
}
/**
* Primary App Landing Page
* @prop {JSON} loc - Location Object {latitude, longitude}
* @prop {Markers[]} markers - Array of Markers
* @prop {JSON} user - User Object
* @returns {Object} - Home Page Component
*/
export function HomePage({ loc, user }) {
return (
<>
<WelcomeMessage user={user} />
<div className="h-[calc(100%-110px)] m-5 rounded-lg">
<Geo
loc={loc}
zoom={14}
moveable={true}
markers={true}
user={user}
/>
</div>
</>
);
}
@@ -0,0 +1,12 @@
/**
* Interest for Profile
* @prop {String} interest - Interest item
* @returns {Object} - Interest Component
*/
export function Interest({ interest }) {
return (
<div>
<div className="rounded-lg m-2 p-2 shadow-xl">{interest}</div>
</div>
);
}
@@ -0,0 +1,139 @@
// System Imports
import { useForm, Form } from "react-hook-form";
// Firebase Imports
import { database, storage } from "../../../../firebase-config";
import { ref as sRef, getDownloadURL,uploadBytes } from "firebase/storage";
import { ref, update } from "firebase/database";
/**
* Profile Edit Component
* @prop {JSON} profileData - Profile Data
* @prop {JSON} user - User Object
* @prop {Function} onSave - Save Function
* @returns {Object} - Profile Edit Component
*/
export function ProfileEdit({ profileData, user, onSave }) {
var { register, control } = useForm();
const handleEditState = () => {
onSave(false);
};
/**
* Handles clicking save button
* @prop {JSON} data - Data to save
*/
function save({ data }) {
// Profile pic handling
if (data.pfp[0]) {
// image stuff
uploadBytes(sRef(storage, `users/${user.uid}/pfp`), data.pfp[0]).then(
() => {
getDownloadURL(sRef(storage, `users/${user.uid}/pfp`)).then((url) => {
data.pfp = url;
for (var key in data) {
if (data[key] == "") {
data[key] = profileData[key];
}
}
handleEditState(false);
update(ref(database, `users/${user.uid}`), data);
});
}
);
} else {
for (var key in data) {
if (data[key] == "") {
data[key] = profileData[key];
}
}
data.pfp = profileData.pfp;
handleEditState(false);
update(ref(database, `users/${user.uid}`), data);
}
}
return (
<div>
<Form onSubmit={save} encType={"application/json"} control={control}>
<div className="grid grid-cols-2">
<div>
<img
src={profileData.pfp}
width="150px"
className="relative mx-auto rounded-2xl overflow-hidden"
/>
Current Profile Picture
</div>
<div className="flex content-center">
<input
type="file"
{...register("pfp")}
className="w-[80%]"
accept=".jpg,.png,.jpeg"
/>
</div>
</div>
<div className="grid grid-cols-2 pl-2 w-[90%]">
<div className="pt-5">
<div className="font-bold">First Name</div>
<input
className="w-[80%] border-2 border-gray-300 p-2 rounded-lg"
type="text"
{...register("firstName")}
placeholder={profileData.firstName}
/>
</div>
<div className="pt-5">
<div className="font-bold">Last Name</div>
<input
className="w-[80%] border-2 border-gray-300 p-2 rounded-lg"
type="text"
{...register("lastName")}
placeholder={profileData.lastName}
/>
</div>
<div className="pt-5">
<div className="font-bold">Username</div>
<input
className="w-[80%] border-2 border-gray-300 p-2 rounded-lg"
type="text"
{...register("username")}
placeholder={profileData.username}
/>
</div>
<div className="pt-5">
<div className="font-bold">Interests (Comma Seperated)</div>
<input
className="w-[80%] border-2 border-gray-300 p-2 rounded-lg"
type="text"
{...register("interests")}
placeholder={profileData.interests}
/>
</div>
<div className="pt-5 col-span-2">
<div className="font-bold">Bio</div>
<textarea
className="w-[92%] border-2 border-gray-300 p-2 rounded-lg"
{...register("bio")}
type="text"
placeholder={profileData.bio}
/>
</div>
<div className="justify-items-center pt-5 col-span-2">
<button
type="submit"
className="p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full text-center"
>
{" "}
Save Changes{" "}
</button>
</div>
</div>
</Form>
</div>
);
}
@@ -0,0 +1,57 @@
// System Imports
import { Popover } from "@headlessui/react";
import Link from "next/link"
// Firebase Imports
import { auth } from "../../../../firebase-config";
import { signOut } from "firebase/auth";
/**
* Logs out from Firebase Authentication
* @returns {void}
*/
function logout() {
signOut(auth);
}
/**
* Profile Panel Component
* @prop {JSON} user - User Object
* @returns {Object} - Profile Panel Component
*/
export function ProfilePanel({user}) {
return (
<Popover className="relative">
<Popover.Button as="div">
<div className="h-[44px] p-[2px] pr-[15px] cursor-pointer bg-cyan-500 text-white font-bold rounded-full shadow-2xl flex">
<div className="flex items-center pl-1">{user.firstName}</div>
<div className="ml-3 rounded-lg">
<img
src={user.pfp}
width="40px"
className="relative mx-auto rounded-xl overflow-hidden"
/>
</div>
</div>
</Popover.Button>
<Popover.Panel className="absolute z-10 bg-white mt-[4px] rounded-xl ml-3 shadow-2xl">
<div className="grid grid-cols-1">
<Link
className="rounded-xl p-4 hover:bg-[#C0C0C0]"
href={"/user?uid=" + user.uid}
>
View Profile
</Link>
<Link
className="rounded-xl p-4 hover:bg-[#C0C0C0]"
onClick={logout}
href="/"
>
Sign Out
</Link>
</div>
</Popover.Panel>
</Popover>
)
}
@@ -0,0 +1,41 @@
// System Imports
import { Geo } from "../map/geo";
import Link from "next/link"
// Component Imports
import { dateOptions } from "../datatypes";
/**
* Profile Room Component
* @prop {JSON} room - Room Object
* @returns {Object} - Profile Room Component
*/
export function ProfileRoom({ room }) {
return (
<div className="rounded-lg p-2 shadow-xl bg-white h-[250px] w-[100%]">
<div className="relative z-1 h-[235px] opacity-50">
<Geo
loc={{ latitude: room.latitude, longitude: room.longitude }}
zoom={12}
moveable={false}
markers={false}
/>
</div>
<div className="relative z-2 top-[-235px] text-left p-2">
<div className="text-2xl font-bold">{room.name}</div>
<div>{room.description}</div>
<div>
Created on {new Date(room.timestamp).toLocaleString(dateOptions)}
</div>
<Link
href={
"/chat?room=" + room.path + "/" + room.name + "-" + room.timestamp
}
className="absolute z-2 top-[190px] w-[108px] p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full flex items-center"
>
Open Room
</Link>
</div>
</div>
);
}
@@ -0,0 +1,67 @@
// Component Imports
import { Geo } from "../map/geo";
import { Member } from "../datatypes"
// Sidebar when in a Chatrooms
/**
* Sidebar while in Chatroom
* @prop {JSON} chatRoomObj - Chatroom Object
* @returns {Object} - Sidebar Component
*/
export function Sidebar({chatRoomObj}) {
// Active users list
if (
chatRoomObj.hasOwnProperty("users") &&
chatRoomObj.users.hasOwnProperty("online")
) {
var activeUsers = [];
var activeUsersJSON = chatRoomObj.users.online;
for (var user in activeUsersJSON)
activeUsers.push(<Member memberObj={activeUsersJSON[user]} />);
var chatroomOnline = activeUsers
}
// Users who added to "my rooms"
if (
chatRoomObj.hasOwnProperty("users") &&
chatRoomObj.users.hasOwnProperty("all")
) {
var allUsers = [];
var allUsersJSON = chatRoomObj.users.all;
for (var user in allUsersJSON)
allUsers.push(<Member memberObj={allUsersJSON[user]} />);
var chatroomUsers = allUsers
}
return (
<div className="overflow-hidden h-dvh">
<div className="m-2 h-[98%] grid grid-cols-1">
<div className="bg-white rounded-lg m-2 shadow-2xl relative">
<div className="w-[100%] h-[100%] opacity-50 absolute rounded-lg z-10">
<Geo
loc={{
latitude: parseFloat(chatRoomObj.latitude.toFixed(2)),
longitude: parseFloat(chatRoomObj.longitude.toFixed(2)),
}}
zoom={12}
moveable={false}
markers={false}
/>
</div>
<div className="z-10 top-0 left-0 w-[100%] h-[100%] absolute text-left pl-3 pt-2">
<span className="font-bold text-[24px]">{chatRoomObj.name}</span>
<br />
{chatRoomObj.description}
</div>
</div>
<div className="bg-white rounded-lg m-2 shadow-2xl">
<div>Online Members</div>
{chatroomOnline}
</div>
<div className="bg-white rounded-lg m-2 shadow-2xl">
<div>All Members</div>
{chatroomUsers}
</div>
</div>
</div>
);
}
@@ -0,0 +1,60 @@
import { Member } from "../datatypes"
import { database } from "../../../../firebase-config"
import {ref, get, set} from "firebase/database"
import { useState, useEffect } from "react"
export function Sidebar({user, chatRoomObj}) {
const [profileData, setProfileData] = useState(null)
// Active users list
if (
chatRoomObj.hasOwnProperty("users") &&
chatRoomObj.users.hasOwnProperty("online")
) {
var activeUsers = [];
var activeUsersJSON = chatRoomObj.users.online;
for (var activeUser in activeUsersJSON)
activeUsers.push(<Member memberObj={activeUsersJSON[activeUser]} />);
var chatroomOnline = activeUsers
}
useEffect(() => {
if (user) {
// Profile Information
if (user.uid == chatRoomObj.UIDs[0]) {
var profileUID = chatRoomObj.UIDs[1]
} else {
var profileUID = chatRoomObj.UIDs[0]
}
get(ref(database, `users/${profileUID}`)).then((snapshot) => {
var profileData = snapshot.val()
setProfileData(profileData)
})
}
}, [user])
return (
<div>
{profileData && (
<div className="overflow-hidden h-dvh">
<div className="m-2 h-[98%] grid grid-cols-1">
<div className="flex place-content-center">
<div className="bg-white rounded-lg m-2 shadow-2xl pt-10">
<img src={profileData.pfp} className="w-[80%] relative mx-auto"/>
<div className="font-bold text-[24px]">{profileData.firstName} {profileData.lastName}</div>
@{profileData.username}
</div>
</div>
<div className="bg-white rounded-lg m-2 shadow-2xl">
<div>In The Chat</div>
{chatroomOnline}
</div>
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,266 @@
// System Imports
import { Form, useForm } from "react-hook-form";
import { useEffect, useState } from "react";
// Dependency Imports
import { Tab } from '@headlessui/react'
// Firebase Imports
import { database } from "../../../../firebase-config";
import { ref, set, get } from "firebase/database";
// Component Imports
import { ChatRoomSidebar } from "../datatypes";
// Friend Imports (TEMP)
import { Friend, FriendRequest } from "../friends/friends";
// DM Imports
import { DM } from "../friends/dm";
/**
* Create Room Component for /app Sidebar
* @prop {JSON} loc - Location Object (latitude, longitude)
* @returns {Object} - Create Room Component
*/
function CreateRoom({ loc }) {
var { register, control, reset, handleSubmit } = useForm();
/**
* Creates Room in Firebase DB
* @prop {JSON} data - Room Data
* @returns {void}
*/
function createRoom(data) {
reset();
var path =
String(loc.latitude.toFixed(2)).replace(".", "") +
"/" +
String(loc.longitude.toFixed(2)).replace(".", "");
var timestamp = new Date().getTime();
var payload = {
name: data.name,
description: data.description,
timestamp: timestamp,
latitude: loc.latitude,
longitude: loc.longitude,
path: path,
};
set(ref(database, `/rooms/${path}/${data.name}-${timestamp}`), payload);
}
return (
<div className="overflow-y-auto h-[90%]">
<Form control={control} onSubmit={handleSubmit(createRoom)}>
<input {...register("name")} placeholder="Room Name" className="mt-2" />
<input
{...register("description")}
placeholder="Room Description"
className="mt-2"
/>
<br />
<div className="mt-3 mb-2">
Creating room near ({loc.latitude.toFixed(2)},{" "}
{loc.longitude.toFixed(2)})
</div>
<button className="p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full mr-5">
Create
</button>
</Form>
</div>
);
}
/**
* Joins class names together for Tailwind CSS
* @param {...String} classes - Class names
* @returns {String} - Class names (joined)
*/
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
/**
* App Page Sidebar Component
* @prop {JSON} user - User Object
* @prop {JSON} location - Location Object (latitude, longitude)
* @prop {Boolean} loadingLoc - Loading Location State
* @returns {Object} - App Page Sidebar Component
*/
export function Sidebar({user,location,loadingLoc}) {
const [nearbyArr, setNearbyArr] = useState([])
const [nearbyArrReady, setNearbyArrReady] = useState(false)
const [friends, setFriends] = useState([])
const [friendRequests, setFriendRequests] = useState(null)
const [dms, setDMs] = useState(null)
// Add myRooms to Sidebar
var myRoomArr = [];
for (var room in user.rooms) {
get(ref(database, `/rooms/${user.rooms[room].path}/${user.rooms[room].name}-${user.rooms[room].timestamp}`)).then((snapshot) => {
var newRoom = (
<ChatRoomSidebar
roomObj={snapshot.val()}
key={snapshot.val().timestamp}
/>
);
myRoomArr.push(newRoom);
})
}
useEffect(() => {
var nearbyArr = []
if (location) {
var path = String(location.latitude.toFixed(2)).replace(".", "") + "/" + String(location.longitude.toFixed(2)).replace(".", "");
get(ref(database, `/rooms/${path}`)).then((snapshot) => {
// Add nearby to Sidebar
if (snapshot.exists()) {
var rooms = snapshot.val()
for (var room in rooms) {
var newRoom = (
<ChatRoomSidebar
roomObj={rooms[room]}
key={rooms[room].timestamp}
/>
);
nearbyArr.push(newRoom);
}
} else {
nearbyArr.push(<div className="pt-5">No Nearby Rooms<br />Create One?</div>)
}
setNearbyArr(nearbyArr)
setNearbyArrReady(true)
})
}
}, [location])
useEffect(() => {
if (user && user.friends) {
get(ref(database, "/users/")).then((snapshot) => {
var users = snapshot.val();
var friends = [];
for (var friend in user.friends.friends) {
friends.push(<Friend user={user} friendObj={users[friend]} key={friend} />);
}
setFriends(friends);
});
var requestArr = [];
for (var request in user.friends.requests) {
get(ref(database, `/users/${request}`)).then((snapshot) => {
requestArr.push(<FriendRequest requestingUser={snapshot.val()} user={user} key={request} />);
});
}
setFriendRequests(requestArr);
} else {
setFriends(<div>No Friends</div>);
setFriendRequests(<div>No Friend Requests</div>);
}
get(ref(database, `/dms`)).then((snapshot) => {
var dmsList = snapshot.val();
var dmArr = [];
for(var dmRoom in dmsList) {
if (user.uid == dmsList[dmRoom].UIDs[0]) {
get(ref(database, `/users/${dmsList[dmRoom].UIDs[1]}`)).then((snapshot) => {
var friendObj = snapshot.val()
dmArr.push(<DM user={user} friendObj={friendObj} key={dmRoom}/>);
})
} else if (user.uid == dmsList[dmRoom].UIDs[1]) {
get(ref(database, `/users/${dmsList[dmRoom].UIDs[0]}`)).then((snapshot) => {
var friendObj = snapshot.val()
dmArr.push(<DM user={user} friendObj={friendObj} key={dmRoom}/>);
})
}
}
if (dmArr.length == 0) {
dmArr.push(<div>No DMs</div>);
}
setDMs(dmArr);
})
}, [user])
return (
<div className="h-dvh bg-[aliceblue] pt-2 pb-2 pl-2 pr-1">
<div className="bg-white rounded-lg h-[98%] mb-[10px] mt-[-18px] mr-2">
<Tab.Group>
<Tab.List className="bg-[#D3D3D3] rounded-lg mt-5">
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)} defaultIndex={1}>Nearby</Tab>
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)}>My Rooms</Tab>
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)}>Create</Tab>
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)} defaultIndex={1}>DMs</Tab>
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)} defaultIndex={1}>Friends</Tab>
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)} defaultIndex={1}>Requests</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<div className="overflow-y-auto h-[90%]">
<div>
{loadingLoc && <div>Loading...</div>}
{nearbyArrReady && nearbyArr}
</div>
</div>
</Tab.Panel>
<Tab.Panel>
<div className="overflow-y-auto h-[90%]">
<div>
{!myRoomArr && <div>No User Saved Rooms</div>}
{myRoomArr}
</div>
</div>
</Tab.Panel>
<Tab.Panel>
{!loadingLoc && <CreateRoom loc={location} />}
{loadingLoc && <div>Loading...</div>}
</Tab.Panel>
<Tab.Panel>
{dms}
</Tab.Panel>
<Tab.Panel>
{friends}
</Tab.Panel>
<Tab.Panel>
{friendRequests}
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
plugins: [],
};
+7
View File
@@ -0,0 +1,7 @@
{
"git": {
"deploymentEnabled": {
"gh-pages": false
}
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB