168 Commits

Author SHA1 Message Date
Nicholas Pease cf2fc469fb Fix Broken Friends on Map (#97) 2024-04-21 21:32:51 -04:00
npease 49bf406795 Fix Broken Friends on Map 2024-04-21 21:31:36 -04:00
Nicholas Pease d75496adcc Add Unit Tests (#95) 2024-04-21 21:15:23 -04:00
Nicholas Pease acfae6b898 Fix Markers on Empty Profiles (#96) 2024-04-21 20:59:48 -04:00
npease 373ff372cb Fix Markers on Empty Profiles 2024-04-21 20:58:08 -04:00
npease 6c54d38efc Add Unit Tests 2024-04-21 16:38:18 -04:00
Nicholas Pease df60173b49 Firebase Optimization (#93) 2024-04-21 13:58:49 -04:00
Nicholas Pease 3be86e6196 Proper Optimization for Firebase (#94) 2024-04-21 02:05:53 -04:00
npease 3d85f9770e Proper Optimization 2024-04-21 02:04:56 -04:00
npease 3af8e3bb6d Firebase Optimization 2024-04-20 23:42:44 -04:00
Nicholas Pease 6ad0f1ad55 Bugfix: My Rooms Loading (#92) 2024-04-19 17:34:59 -04:00
Nicholas Pease 6fb88acf44 Merge branch 'main' into npease-bugfix-room-load 2024-04-19 11:34:33 -04:00
npease d98de59fcf Bugfix: My Rooms Loading 2024-04-19 11:33:57 -04:00
Nicholas Pease ca691d2203 DM Security Fix (#91) 2024-04-19 11:29:21 -04:00
Nicholas Pease 31842373b8 Multiple Mapping Improvements (#90) 2024-04-19 11:25:32 -04:00
npease fcdc95505e Straggler 2024-04-19 11:24:34 -04:00
Nicholas Pease af55b7e58d Merge branch 'main' into npease-mapping-improvements 2024-04-19 11:21:19 -04:00
npease 094cdab65e DM Security Fix 2024-04-19 11:18:46 -04:00
Nicholas Pease 059a90808a Add Search to Nearby Tab (#89) 2024-04-19 11:13:03 -04:00
Nicholas Pease ea6801fbb5 Fix Image Detection Async (#88) 2024-04-19 11:12:43 -04:00
npease 925c6f28fe Map Improvements 2024-04-19 11:09:12 -04:00
npease 75a3919eb2 Added Friends, My Rooms, Refactored Nearby Markers Functions 2024-04-19 10:01:04 -04:00
npease 803432db8e Linting Fixes 2024-04-19 09:13:41 -04:00
npease b8a732eea5 Add Nearby Rooms Search 2024-04-19 09:08:48 -04:00
npease 294dd3a6b0 Image Fix Async 2024-04-19 02:32:18 -04:00
Nicholas Pease de0c9e9785 Circle Icon Fix (#87) 2024-04-19 02:09:09 -04:00
npease 45f9ec19d1 Circle Icon Fix 2024-04-19 02:08:03 -04:00
Nicholas Pease f73cd7f07a Better Picture Detection / Handling (#86) 2024-04-19 02:01:55 -04:00
Nicholas Pease fb19f53d63 Fix Delete (#85) 2024-04-19 02:01:42 -04:00
npease f1e23ac056 Better Picture Detection / Handling 2024-04-19 01:59:43 -04:00
npease 50308ce43c Fix Delete 2024-04-19 00:09:51 -04:00
Nicholas Pease d7d47bae9e Fix disappearing rooms on status visibility change (#84) 2024-04-18 23:49:26 -04:00
npease e203a87a77 Fix disappearing rooms on status visibility change 2024-04-18 23:29:47 -04:00
npease 5f089341be Clear Error From Console 2024-04-18 23:24:57 -04:00
Nicholas Pease f2c9d55a20 General Bug Fixes (#83) 2024-04-18 23:17:04 -04:00
npease 04c0cb4cff Deleted Key Variable 2024-04-18 23:15:26 -04:00
npease 32dda6fe8d Cleanup Console.Logs 2024-04-18 23:14:33 -04:00
Nicholas Pease da7320255e Fix Message Parsing Issues (#82) 2024-04-18 23:12:31 -04:00
npease e8fad393f7 Fix Message Parsing Issues 2024-04-18 23:11:38 -04:00
Nicholas Pease 02a9eeaa36 Add User Message Deletion (#78) 2024-04-18 22:39:55 -04:00
Nicholas Pease c4374da5ba Merge branch 'main' into npease-chat-message-deletion 2024-04-18 22:39:14 -04:00
Nicholas Pease 2a33838b93 Fix Profile Picture Display (#81) 2024-04-18 14:41:42 -04:00
Nicholas Pease 527dcd8660 Properly Integrate Images, RMF and Profane filters (#80) 2024-04-18 14:40:31 -04:00
npease d1a9116b34 Fix Profile Picture Scaling on Different Screen Sizes 2024-04-18 13:01:41 -04:00
npease 9ee81aa487 Profile Picture Sizing Corrected on Sidebar 2024-04-18 12:59:12 -04:00
npease de99e26ca2 Properly Integrate Images, RMF and Profane filters 2024-04-18 12:47:02 -04:00
Nicholas Pease 2822152799 Add Image Detection to RMF (Rich Message Formatting) (#79) 2024-04-18 01:19:47 -04:00
Nicholas Pease baa754eeb8 Add Profanity Filter (#77) 2024-04-18 01:18:36 -04:00
Nicholas Pease fe90a39983 Add Global User Online / Offline (#76) 2024-04-18 01:18:16 -04:00
npease 5eb2002d8e Install in proper directory 2024-04-18 00:07:29 -04:00
npease 3446ea47b4 Add dependency to package.json 2024-04-18 00:05:43 -04:00
npease 32a5f5b057 Add Image Detection to RMF (Rich Message Formatting) 2024-04-18 00:01:17 -04:00
npease 8f00bade79 Add message deletion for users messages only 2024-04-17 23:24:08 -04:00
npease c3d55c94d1 Add Profanity Filter 2024-04-17 22:46:55 -04:00
npease 7c00b49870 Simple Fix for Blank Messages 2024-04-17 22:27:25 -04:00
npease 5b161ad232 Add Profile Menu Option to Appear Offline 2024-04-17 22:18:20 -04:00
npease 7954432add Add Global User Online Status 2024-04-17 22:06:24 -04:00
Sgoodridge96 76f18d2b15 Sgoodridge (#75) 2024-04-11 01:46:29 -04:00
Stephen a695a47d8b Merge branch 'main' into sgoodridge 2024-04-11 01:32:03 -04:00
Stephen 401cc4670c Fix profile pic bugs 2024-04-11 01:29:12 -04:00
Nicholas Pease 0668350eed Add Privacy Policy (For Google App Store) (#74) 2024-04-10 21:33:53 -04:00
npease ad39b54f1d Add Deletion Info Page 2024-04-10 14:14:20 -04:00
npease 398725e7b7 Add Privacy Police (For Google App Store) 2024-04-10 14:07:31 -04:00
ClarkLach f726d6d40f Map Marker Tweaks (#73) 2024-04-09 21:47:03 -04:00
ClarkLach 1e3c0ce930 Custom Icons POC
Will need to pick out some good icons/styles. Overlay offset needs some work still.
2024-04-09 20:33:49 -04:00
ClarkLach 0eb766b2a0 Hover effect for map rooms
Hopefully nothing is broken, some formatting still needed
2024-04-09 15:46:19 -04:00
Nicholas Pease 6c9f4af2dd Various Bug Fixes (#72) 2024-04-08 10:26:52 -04:00
npease ed6c1b427e Bug Fix - JSDoc Build Error 2024-04-08 00:13:14 -04:00
npease bc25b0e694 Bug Fix - Unauth Redirect 2024-04-08 00:08:21 -04:00
npease 3e354800dd Bug Fix - DM List 2024-04-07 23:59:29 -04:00
npease c3c76b66c9 Bug Fix - Restore Undid Change 2024-04-07 23:45:12 -04:00
npease ce5add2879 Bug Fix - Build Error 2024-04-07 23:33:27 -04:00
npease 5629dc8836 Bug Fix - Profile Editing 2024-04-07 23:31:18 -04:00
npease 0d4787ccfb Bug Fix - Profile Loading 2024-04-07 22:37:35 -04:00
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 981d17166e Dev Branch Update (#45) 2024-03-22 16:12:23 -04:00
85 changed files with 13123 additions and 1099 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
+3
View File
@@ -1,3 +1,6 @@
# Android Build
/frontend-next/android
# Firebase Stuff
firebase-admin-key.json
firebase*.json
+4 -4
View File
@@ -1,9 +1,9 @@
![](/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 will implement 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.
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 others, as well as a chat room feature that allows users to start public conversations based on any given 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 new 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 will implement user login and profiles, allowing users to add each other as friends and start private conversations. There will be several default chat rooms of varying topics, but users will 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 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 rooms that will be visible to other users. For example, a user at the University of Maine could create a joinable chat room titled “COS420”, which would be joinable by 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.
@@ -11,11 +11,11 @@ The live version of this app can be found at:
https://chatma.ps/
A local version can be run with:
A local version can be run (assuming you have Node installed) with:
cd frontend-next/
npm run build
npm install
npm run dev
Binary file not shown.
+3 -2
View File
@@ -1,8 +1,9 @@
{
"extends": "next/core-web-vitals",
"extends": ["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"
"@next/next/no-img-element": "off",
"no-console": 1
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Chat } from "../src/components/app/datatypes";
import renderer from 'react-test-renderer';
var exampleChatObj = {
body: "Hello, World!",
isSystem: false,
timestamp: 1710527946340,
user: "TestUser"
}
var exampleUser = {
uid: "123456",
username: "TestUser",
lastOnline: true
}
it('Chat Renders Correctly', () => {
const tree = renderer
.create((<Chat chatObj={exampleChatObj} user={exampleUser} />))
.toJSON();
expect(tree).toMatchSnapshot();
});
@@ -0,0 +1,14 @@
import { ChatRoomSidebar } from "../src/components/app/datatypes";
import renderer from 'react-test-renderer';
var exampleRoom = {
name: "TestRoom",
description: "This is a test room.",
}
it('ChatRoomSidebar Renders Correctly', () => {
const tree = renderer
.create((<ChatRoomSidebar roomObj={exampleRoom} />))
.toJSON();
expect(tree).toMatchSnapshot();
});
+30
View File
@@ -0,0 +1,30 @@
import { DM } from "../src/components/app/friends/dm";
import renderer from 'react-test-renderer';
var message = "Hello, World! This is a test message. https://www.google.com"
var exampleUser = {
uid: "123456",
username: "TestUser",
firstName: "Test",
lastName: "User",
lastOnline: true,
pfp: "https://th.bing.com/th/id/OIP.K5VoTfw97JiEc1OBODAjmQHaHO?rs=1&pid=ImgDetMain"
}
var friendObj = {
uid: "123456",
username: "TestUser",
firstName: "Test",
lastName: "Friend",
lastOnline: true,
pfp: "https://th.bing.com/th/id/OIP.K5VoTfw97JiEc1OBODAjmQHaHO?rs=1&pid=ImgDetMain"
}
it('RMF Renders Correctly', () => {
const tree = renderer
.create(<DM user={exampleUser} friendObj={friendObj} />)
.toJSON();
expect(tree).toMatchSnapshot();
});
+25
View File
@@ -0,0 +1,25 @@
import { FirstPage } from "@mui/icons-material";
import { Member } from "../src/components/app/datatypes";
import renderer from 'react-test-renderer';
var exampleChatObj = {
body: "Hello, World!",
isSystem: false,
timestamp: 1710527946340,
user: "TestUser"
}
var exampleUser = {
uid: "123456",
username: "TestUser",
firstName: "Test",
lastName: "User",
lastOnline: true
}
it('Member Renders Correctly', () => {
const tree = renderer
.create((<Member memberObj={exampleUser} />))
.toJSON();
expect(tree).toMatchSnapshot();
});
+11
View File
@@ -0,0 +1,11 @@
import { RMF } from "../src/components/app/datatypes";
import renderer from 'react-test-renderer';
var message = "Hello, World! This is a test message. https://www.google.com"
it('RMF Renders Correctly', () => {
const tree = renderer
.create((RMF(message)))
.toJSON();
expect(tree).toMatchSnapshot();
});
@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Chat Renders Correctly 1`] = `
<div
className="width-[100%] bg-white rounded-lg mt-1 text-left p-1 grid grid-cols-2 mr-2"
>
<div>
<span
className="mr-[5px]"
style={
{
"color": "#133337",
}
}
>
<a
className="hover:font-bold cursor-pointer"
href="/user?uid=undefined"
onClick={[Function]}
onMouseEnter={[Function]}
onTouchStart={[Function]}
>
TestUser
</a>
</span>
Hello, World!
</div>
<div
className="text-right text-[#d1d1d1]"
>
3/15/2024, 2:39:06 PM
</div>
</div>
`;
@@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChatRoomSidebar Renders Correctly 1`] = `
<div
className="border-[black] border-1 shadow-lg p-2 m-2 rounded-lg cursor-pointer"
>
<a
href="/chat?room=undefined/TestRoom-undefined"
onClick={[Function]}
onMouseEnter={[Function]}
onTouchStart={[Function]}
>
<div
className="grid grid-cols-3"
>
<div
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
data-testid="PersonIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4m0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4"
/>
</svg>
<div>
0
/
0
</div>
</div>
<div
className="col-span-2"
>
<div
className="font-bold"
>
TestRoom
</div>
<div
className="italic"
>
This is a test room.
</div>
</div>
</div>
</a>
</div>
`;
@@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RMF Renders Correctly 1`] = `
<div
className="border-[black] border-1 shadow-lg m-2 rounded-lg"
>
<div
className="grid grid-cols-4"
>
<div
className="place-content-center"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium cursor-pointer css-i4bv87-MuiSvgIcon-root"
data-testid="ChatIcon"
focusable="false"
onClick={[Function]}
viewBox="0 0 24 24"
>
<path
d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2M6 9h12v2H6zm8 5H6v-2h8zm4-6H6V6h12z"
/>
</svg>
</div>
<div
className="col-span-3 cursor-pointer"
>
<div
onClick={[Function]}
>
<div
className="grid grid-cols-2 justify-items-center"
>
<div
className="mr-8"
>
<img
className="w-[50px] h-[50px]"
src="https://th.bing.com/th/id/OIP.K5VoTfw97JiEc1OBODAjmQHaHO?rs=1&pid=ImgDetMain"
/>
</div>
<div
className=""
>
<div
className="font-bold"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSize20px text-lime-600 mr-1 relative top-[-2px] css-821wyw-MuiSvgIcon-root"
data-testid="CircleIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2"
/>
</svg>
Test
Friend
</div>
<div
className=""
>
@
TestUser
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Member Renders Correctly 1`] = `
<a
href="/user?uid=123456"
onClick={[Function]}
onMouseEnter={[Function]}
onTouchStart={[Function]}
>
<div
className="cursor-pointer g-[aliceblue] rounded-lg m-3 shadow-xl p-2"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSize20px text-lime-600 mr-1 relative top-[-1px] css-821wyw-MuiSvgIcon-root"
data-testid="CircleIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2"
/>
</svg>
TestUser
</div>
</a>
`;
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RMF Renders Correctly 1`] = `
<span
className="mr-2"
>
Hello, World! This is a test message.
<img
className="max-w-[100%]"
src="https://www.google.com"
/>
</span>
`;
+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;
@@ -1,6 +1,7 @@
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",
@@ -14,6 +15,7 @@ var config = {
var app = getApps().length > 0 ? getApp() : initializeApp(config);
var auth = getAuth(app);
var storage = getStorage(app);
var database = getDatabase(app);
export { auth, app, database };
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

+204
View File
@@ -0,0 +1,204 @@
const nextJest = require('next/jest')
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
/** @type {import('jest').Config} */
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
})
const config = {
testEnvironment: 'jsdom',
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "C:\\Users\\Nick\\AppData\\Local\\Temp\\jest",
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "\\\\node_modules\\\\",
// "\\.pnp\\.[^\\\\]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
module.exports = createJestConfig(config)
+1 -1
View File
@@ -1,4 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {output: 'export'};
export default nextConfig;
+9557 -53
View File
File diff suppressed because it is too large Load Diff
+24 -3
View File
@@ -5,22 +5,43 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"start": "npx serve@latest out",
"lint": "next lint",
"test": "jest"
},
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/preset-env": "^7.24.4",
"@babel/preset-react": "^7.24.1",
"@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",
"bad-words": "^3.0.4",
"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-hook-form": "^7.50.1"
"react-firebase-hooks": "^5.1.1",
"react-hook-form": "^7.50.1",
"react-test-renderer": "^18.2.0"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@capacitor/cli": "^5.7.4",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^15.0.2",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8",
"tailwindcss": "^3.3.0"
}
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>User Data Deletion Request</title>
</head>
<body>
<h1>User Data Deletion Request</h1>
<p>
To request the deletion of your user data from our system, please follow the steps below:
</p>
<ol>
<li>Compose an email to <a href="mailto:deletion@chatma.ps">deletion@chatma.ps</a>.</li>
<li>In the email subject, write "User Data Deletion Request".</li>
<li>In the body of the email, provide the following information:</li>
<ul>
<li>Your username: [Insert your username here]</li>
<li>Your email address associated with the account: [Insert your email address here]</li>
</ul>
<li>Send the email.</li>
</ol>
<p>
Our team will process your request as soon as possible. Please note that the deletion of your user data may take some time, and we will notify you once it has been completed.
</p>
<p>
If you have any further questions or concerns, please don't hesitate to contact us at <a href="mailto:support@chatma.ps">support@chatma.ps</a>.
</p>
</body>
</html>
+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"
}
+33
View File
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ChatMaps Privacy Policy</title>
</head>
<body>
<h1>ChatMaps Privacy Policy</h1>
<p>At ChatMaps, we are committed to protecting your privacy and ensuring the security of your personal information. This privacy policy explains how we collect, use, and safeguard your data when you use our application.</p>
<h2>Collection of Personal Information</h2>
<p>ChatMaps requires access to your location in order to provide you with location-based services. However, we do not share, sell, or disclose your personal data to any third party.</p>
<h2>Child Safety</h2>
<p>ChatMaps is fully compliant with online child safety laws. We do not knowingly collect personal information from children under the age of 13. If you believe that we have inadvertently collected personal information from a child, please contact us immediately so that we can take appropriate action.</p>
<h2>Data Security</h2>
<p>We take the security of your personal information seriously. We implement industry-standard security measures to protect your data from unauthorized access, alteration, or disclosure.</p>
<h2>Changes to this Privacy Policy</h2>
<p>We reserve the right to update or modify this privacy policy at any time. Any changes will be effective immediately upon posting the updated policy on our website.</p>
<h2>Contact Us</h2>
<p>If you have any questions or concerns about our privacy policy, please contact us at privacy@chatma.ps .</p>
</body>
</html>
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

+1 -3
View File
@@ -11,9 +11,7 @@ export const metadata = {
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
<body className={inter.className}>{children}</body>
</html>
);
}
+85 -259
View File
@@ -1,292 +1,118 @@
"use client";
// System Imports
import { useState, useEffect } from "react";
import { auth, database } from "../api/firebase-config";
import { ref, onValue, set, remove, get } from "firebase/database";
import { useBeforeunload } from "react-beforeunload";
import {useRouter} from "next/navigation";
import {Marker} from "pigeon-maps";
import {onAuthStateChanged, signOut} from "firebase/auth"
// Refactored Component Imports
// Data Structure Imports
import { ChatRoomSidebar, Member } from "../../components/app/datatypes";
// Dependencies
import Drawer from '@mui/material/Drawer';
// Header Import
// Firebase Imports
import { auth, database } from "../../../firebase-config";
import { ref, onValue, set } 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";
// Main Tab Imports
import { MainTabChatRoom } from "../../components/app/main_tab/chat";
import { MainTabHome } from "../../components/app/main_tab/home";
// Capacitor Import
import { Geolocation } from '@capacitor/geolocation';
// Sidebar Imports
import {Home_Sidebar} from "../../components/app/sidebar/home";
import { Chat_Sidebar } from "../../components/app/sidebar/chat";
import { Profile_Sidebar } from "../../components/app/sidebar/profile";
// Contains most everything for the app homepage
/**
* Contains most everything for the app homepage
* @returns {Object} Home Page
*/
function Home() {
// It's time to document and change these awful variable names
// State variables for app page
const [mainTab, setMainTab] = useState("home"); // Primary tab
const [tab, setTab] = useState("nearby"); // Sidebar Tab
const [chatRoomObj, setChatRoomObj] = useState(null); // Current chatroom object
const [myRoomsObj, setMyRoomsObj] = useState(null); // My Rooms Object
const [myRooms, setRoomData] = useState(null); // Current user saved rooms list
const [isRoomLoading, setRoomLoading] = useState(true); // myRooms loading variable, true = myRooms loading, false = finished loading
const [isMyRoom, setIsMyRoom] = useState(false); // Is current room in myRooms? true, false
const [location, setLocation] = useState(null); // location variable [lat,long]
const [user, setUser] = useState(null); // user data
const [loadingLoc, setLoadingLoc] = useState(true); // location variable loading, true = loading, false = finished loading
const [nearby, setNearby] = useState(null); // nearby rooms array
const [loadingNearby, setLoadingNearby] = useState(true); // loading nearby rooms array, true = loading, false = finished loading
const [chatroomOnline, setChatRoomOnline] = useState(null); // holds online users
const [chatroomUsers, setChatroomUsers] = useState(null); // holds all chatroom users
const [chatroomUsersLoading, setChatroomUsersLoading] = useState(true);
const [markers, setMarkers] = useState([]);
const [isAuthenticated, setAuth] = useState(false)
const [user, setUser] = useState(null)
const [authUser, authLoading] = useAuthState(auth) // auth user object (used to obtain other user object)
const [drawerOpen, setDrawerOpen] = useState(true); // drawer open state
const [coords, setCoords] = useState(null)
// Authentication
var windowSize = useWindowSize()
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
get(ref(database, `users/${user.uid}`))
.then((userData) => {
userData = userData.val()
console.log(userData)
if (userData) {
setUser(userData)
setAuth(true)
} else {
window.location.href = "/onboarding"
}
})
} else {
setAuth(false)
window.location.href = "/login"
}
})
if (windowSize.width < 767) {
setDrawerOpen(false)
} else {
setDrawerOpen(true)
}
}, [windowSize])
// Authentication Verification / Redirection if Profile Data not Filled out
useEffect(() => {
if (authUser && authLoading === false) {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
setUser({...userData});
} else {
window.location.href = "/onboarding";
}
});
} else if (authLoading === false) {
window.location.href = "/login";
}
}, [authLoading])
useEffect(() => {
Geolocation.getCurrentPosition().then((position) => {
setCoords(position.coords);
setLoadingLoc(false);
});
}, [])
// Grabs user data, saves to user, then lists the users saved rooms
// Saves users last loc to profile for friends map
useEffect(() => {
if (user) {
onValue(ref(database, "/users/" + user.uid + "/rooms"), (snapshot) => {
setRoomLoading(true);
var rooms = snapshot.val();
setMyRoomsObj(rooms);
var roomArr = [];
var markerArr = markers;
for (var room in rooms) {
var newRoom = (
<ChatRoomSidebar
roomObj={rooms[room]}
key={rooms[room].timestamp}
click={selectChatRoom}
/>
);
markerArr.push(
<Marker
width={30}
anchor={[rooms[room].latitude, rooms[room].longitude]}
color="blue"
/>
);
roomArr.push(newRoom);
}
setMarkers(markerArr);
setRoomData(roomArr);
setRoomLoading(false);
});
}
},[user]);
// Grabs the user location
useEffect(() => {
if ("geolocation" in navigator && user) {
// Retrieve latitude & longitude coordinates from `navigator.geolocation` Web API
navigator.geolocation.getCurrentPosition(({ coords }) => {
setLocation(coords)
setLoadingLoc(false)
var path = String(coords.latitude.toFixed(2)).replace(".","")+"/"+String(coords.longitude.toFixed(2)).replace(".","")
var markersArr = markers
onValue(ref(database, `/rooms/${path}`), (snapshot) => {
var nearbyArr = []
if (snapshot.exists()) {
var data = snapshot.val();
for (var room in data) {
nearbyArr.push(
<ChatRoomSidebar roomObj={data[room]} click={selectChatRoom} />
);
// TODO: RANDOM LAST DIGIT TO MOVE AROUND THE MAP
markersArr.push(
<Marker
width={30}
anchor={[data[room].latitude, data[room].longitude]}
color="blue"
/>
);
}
setMarkers(markersArr);
setLoadingNearby(false);
setNearby(nearbyArr);
} else {
setLoadingNearby(false);
}
});
});
if (coords && user) {
set(ref(database,`users/${user.uid}/location`), {
latitude: coords.latitude,
longitude: coords.longitude
})
}
},[user]);
// Dont Double Send Leaving Message
useEffect(() => {
if (myRoomsObj && chatRoomObj) {
var roomName = chatRoomObj.name + "-" + chatRoomObj.timestamp;
if (myRooms != null && roomName in myRoomsObj) {
// its in there
setIsMyRoom(true);
} else {
// its not in there
setIsMyRoom(false);
}
}
}, [chatRoomObj]);
// Selects chat room
function selectChatRoom(roomObj) {
// Path of chatroom
var path = roomObj.path + "/" + roomObj.name + "-" + roomObj.timestamp;
setChatRoomObj(roomObj);
// Send entered message
var payload = {
body: "entered",
user: user.username,
isSystem: true,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/rooms/${path}/chats/${new Date().getTime()}-${user.username}`
),
payload
);
// Code for Room Data
set(ref(database, `/rooms/${path}/users/online/${user.uid}`), user);
onValue(ref(database, `/rooms/${path}`), (snapshot) => {
setChatRoomOnline(null);
setChatroomUsers(null);
// Active users list
if (
snapshot.val().hasOwnProperty("users") &&
snapshot.val().users.hasOwnProperty("online")
) {
var activeUsers = [];
var activeUsersJSON = snapshot.val().users.online;
for (var user in activeUsersJSON)
activeUsers.push(<Member memberObj={activeUsersJSON[user]} />);
setChatRoomOnline(activeUsers);
}
// Users who added to "my rooms"
console.log(
snapshot.val().hasOwnProperty("users") &&
snapshot.val().users.hasOwnProperty("all")
);
if (
snapshot.val().hasOwnProperty("users") &&
snapshot.val().users.hasOwnProperty("all")
) {
setChatroomUsersLoading(true);
var allUsers = [];
var allUsersJSON = snapshot.val().users.all;
for (var user in allUsersJSON)
allUsers.push(<Member memberObj={allUsersJSON[user]} />);
setChatroomUsers(allUsers);
setChatroomUsersLoading(false);
}
});
setMainTab("chat");
}
// Fires to tell other uses that you are leaving the room
useBeforeunload(() => {
if (chatRoomObj && mainTab == "chat") {
var payload = {
body: "left",
user: user.username,
isSystem: true,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/rooms/${
chatRoomObj.path +
"/" +
chatRoomObj.name +
"-" +
chatRoomObj.timestamp
}/chats/${new Date().getTime()}-${user.username}`
),
payload
);
remove(
ref(
database,
`/rooms/${
chatRoomObj.path +
"/" +
chatRoomObj.name +
"-" +
chatRoomObj.timestamp
}/users/online/${userID}`
)
);
}
});
}, [coords, user])
return (
<div>
{isAuthenticated && (
<div className="grid grid-cols-4 auto-cols-max overflow-hidden">
<div className="overflow-hidden h-dvh">
{user && (
<div className="overflow-hidden h-dvh">
{/* Left Side of Page */}
<div className="col-span-3 h-dvh">
<div className="overflow-hidden h-dvh md:mr-[405px]">
{/* Header */}
<Header mainTab={mainTab} isMyRoom={isMyRoom} chatRoomObj={chatRoomObj} setChatRoomObj={setChatRoomObj} setMainTab={setMainTab} setIsMyRoom={setIsMyRoom} user={user}/>
<Header
mainTab={"home"}
user={user}
sidebarControl={() => {setDrawerOpen(!drawerOpen)}}
/>
{/* Main Page Section */}
<div className="mr-2 h-[calc(100%-110px)]">
{mainTab == "home" && !loadingLoc && (
<MainTabHome loc={location} markers={markers} user={user}/>
{!loadingLoc && (
<HomePage loc={coords} user={user} />
)}
{mainTab == "home" && loadingLoc && (
<MainTabHome loc={null} markers={markers} user={user}/>
{loadingLoc && (
<HomePage loc={null} user={user} />
)}
{mainTab == "chat" && <MainTabChatRoom roomObj={chatRoomObj} user={user}/>}
</div>
</div>
{/* Sidebar (Right Side of Page) */}
{mainTab == "home" && (
<Home_Sidebar tab={tab} nearby={nearby} loadingNearby={loadingNearby} setTab={setTab} isRoomLoading={isRoomLoading} myRooms={myRooms} loadingLoc={loadingLoc} location={location}/>
)}
{mainTab == "chat" && (
<Chat_Sidebar chatRoomObj={chatRoomObj} chatroomOnline={chatroomOnline} chatroomUsersLoading={chatroomUsersLoading} chatroomUsers={chatroomUsers}/>
)}
{mainTab == "profile" && (
<Profile_Sidebar/>
)}
<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>
</div>
);
}
export default Home;
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>
);
}
+144
View File
@@ -0,0 +1,144 @@
"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, get, onChildAdded, onChildRemoved } 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, authLoading] = 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 && authLoading === false) {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
setUser(userData);
} else {
window.location.href = "/onboarding";
}
});
} else if (authLoading === false) {
window.location.href = "/login";
}
}, [authLoading])
// 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,
})*/
// Room Object Load
get(ref(database, `/rooms/${path}`)).then((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>
);
}
+146
View File
@@ -0,0 +1,146 @@
"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, get, onChildAdded, onChildRemoved} 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, authLoading] = useAuthState(auth) // auth user object (used to obtain other user object)
const [drawerOpen, setDrawerOpen] = useState(true); // drawer open state
const [isUserAuthed, setIsUserAuthed] = useState(false); // is the user authenticated or not
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 && authLoading === false) {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
setUser(userData);
} else {
window.location.href = "/onboarding";
}
});
} else if (authLoading === false) {
window.location.href = "/login";
}
}, [authLoading])
// 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")
if (path.includes(user.uid))
setIsUserAuthed(true)
else
location.href = "/app"
/*// 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,
})*/
// Room Object Load
get(ref(database, `/dms/${path}`)).then((roomData) => {
roomData = roomData.val();
setChatRoomObj(roomData)
if (!doneLoading) {
setDoneLoading(true)
}
})
}
}, [user]);
return (
<div>
{(authUser && doneLoading && isUserAuthed) && (
<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;
+1 -1
View File
@@ -5,7 +5,7 @@
body {
background-color: aliceblue;
text-align: center;
text-wrap:pretty;
text-wrap: pretty;
}
button {
+2 -4
View File
@@ -5,15 +5,13 @@ const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps",
description: "ChatMaps: Social Media for College Students"
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
<body className={inter.className}>{children}</body>
</html>
);
}
+1 -3
View File
@@ -11,9 +11,7 @@ export const metadata = {
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
<body className={inter.className}>{children}</body>
</html>
);
}
+123 -54
View File
@@ -1,66 +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";
import "../globals.css"
// Firebase imports
import {auth} from "../api/firebase-config";
import { setPersistence, signInWithEmailAndPassword, browserSessionPersistence } from "firebase/auth";
// 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, handleSubmit } = useForm();
var { register, control, setError, handleSubmit, formState: { errors, isSubmitting, isSubmitted } } = useForm()
var router = useRouter();
var {register,control,setError,reset,formState: { errors, isSubmitting, isSubmitted },} = useForm();
function authenticate(data) {
setPersistence(auth, browserSessionPersistence)
.then(() => {
signInWithEmailAndPassword(auth,data.email,data.password)
.then((userCredential) => {
if (userCredential.user) {
router.push("/app")
} else {
const formError = { type: "server", message: "Username or Password Incorrect" }
// set same error in both:
setError('password', formError)
setError('email', formError)
}
})
})
}
/**
* 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 (
return (
<div>
<div className="grid h-screen place-items-center">
<div>
<div className="grid h-screen place-items-center">
<div>
<a href="/"><img src="logos/logo_transparent_inverse.png"/></a>
<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={handleSubmit(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 className="inline-flex items-center px-4 py-2 transition ease-in-out duration-150 bg-[#dee0e0] 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? <a href="/register">Sign Up</a><br/>
</Form>
</div>
</div>
</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;
export default Login;
+2 -4
View File
@@ -5,15 +5,13 @@ const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Onboarding",
description: "ChatMaps: Social Media for College Students"
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
<body className={inter.className}>{children}</body>
</html>
);
}
+78 -49
View File
@@ -1,58 +1,87 @@
"use client";
import "../globals.css"
// 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 "../api/firebase-config"
import {onAuthStateChanged} from "firebase/auth"
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) {
console.log(user)
data.uid = user.uid
data.defined = true
data.email = user.email
set(ref(database, `users/${user.uid}`), data);
return true
} else {
return false
}
})
}
function Onboarding() {
var router = useRouter();
var { register, handleSubmit } = useForm();
function Onboard(data) {
createUser(data)
router.push("/app");
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;
}
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 px-4 py-2 transition ease-in-out duration-150 bg-[#dee0e0] m-5 bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">Save</button>
</form>
</div>
</div>
</div>
)
});
}
export default Onboarding;
/**
* 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;
+96 -59
View File
@@ -1,74 +1,111 @@
"use client"
import { useState, useEffect } from 'react'
import { auth, database } from "./api/firebase-config";
import { ref, get} from "firebase/database";
import {onAuthStateChanged} from "firebase/auth"
"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)
const [roomCount, setRoomCount] = useState(null)
const [isAuthenticated, setAuth] = useState(false)
const [userID, setUserID] = useState(null)
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
// Authentication
useEffect(() => {
onAuthStateChanged(auth, (user) => {
onAuthStateChanged(auth, async (user) => {
if (user) {
setUserID(user.uid)
setAuth(true)
setAuth(true);
} else {
setAuth(false)
setAuth(false);
}
})
});
}, []);
useEffect(() => {
Geolocation.getCurrentPosition().then((position) => {
setCoords(position.coords);
setLoadingLoc(false);
});
}, [])
useEffect(() => {
if('geolocation' in navigator) {
// Retrieve latitude & longitude coordinates from `navigator.geolocation` Web API
navigator.geolocation.getCurrentPosition(({ 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 {
console.log("No rooms nearby")
setRoomCount(0)
}
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;
}
})
return (
setRoomCount(count);
} else {
setRoomCount(0);
}
setLoadingLoc(false);
});
}
}, [coords])
return (
<div>
<div className="grid h-screen place-items-center">
<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>
<a href="/login"><button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">Login</button></a>
<a href="/register"><button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">Sign Up</button></a>
</div>
}
{isAuthenticated && <a href="/app"><button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">Continue to App</button></a>}
{(!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>
<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;
export default Home;
+1 -3
View File
@@ -11,9 +11,7 @@ export const metadata = {
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
<body className={inter.className}>{children}</body>
</html>
);
}
+134 -65
View File
@@ -1,78 +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 "../globals.css"
import { useState } from "react";
import Link from "next/link"
import { createUserWithEmailAndPassword, signInWithEmailAndPassword, setPersistence, browserSessionPersistence } from "firebase/auth";
import {auth} from "../api/firebase-config";
// 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, browserSessionPersistence)
.then(() => {
signInWithEmailAndPassword(auth,data.email,data.password)
.then((res) => {
console.log(res)
return true
})
})
} else {
return false
}
}
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);
const passwordMatch = (data) => {
return data.password === data.passwordCheck;
};
function onSubmit({data}) {
if (passwordMatch(data)) {
setPasswordMismatch(false);
if (Signup(data)) {
router.push("/onboarding");
}
} else{
setPasswordMismatch(true);
return;
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;
};
return (
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>
<div className="grid h-screen place-items-center">
<div>
<a href="/"><img src="logos/logo_transparent_inverse.png"/></a>
<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>}
<button type="submit" className="bg-[#dee0e0] m-5 bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">
Register</button><br/>
Have an account? <a href="/login">Log In</a>
</Form>
</div>
</div>
</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>
);
}
+182
View File
@@ -0,0 +1,182 @@
"use client";
// System Imports
import { useState, useEffect } from "react";
import { auth, database } from "../../../firebase-config";
import { ref, onValue, get } from "firebase/database";
import { useAuthState } from "react-firebase-hooks/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";
// Icons
import CircleIcon from '@mui/icons-material/Circle';
/**
* 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?
const [authUser, authLoading] = useAuthState(auth) // auth user object (used to obtain other user object)
// Handles Edit State in Component, shares useState with ProfileEdit
const [isEditing, setIsEditing] = useState(false);
const handleIsEditing = (newValue) => {
setIsEditing(newValue);
};
// Authentication
useEffect(() => {
if (authUser && authLoading === false) {
const searchParams = new URLSearchParams(document.location.search);
var userUID = searchParams.get("uid")
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
if (userData.uid == userUID) {
setIsOwner(true);
}
setIsAuthenticated(true);
setUser(userData);
} else {
window.location.href = "/onboarding";
}
});
} else if (authLoading === false) {
window.location.href = "/login";
}
}, [authLoading]);
// 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 && profileData) && (
<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}
className="relative mx-auto rounded-2xl overflow-hidden w-[90%]"
/>
<div className="font-bold text-[30px] flex justify-center items-center">
{profileData.lastOnline == true && <CircleIcon className="text-lime-600 mr-3"/>}{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;
+241 -118
View File
@@ -1,142 +1,265 @@
import { Map, Marker, ZoomControl } from "pigeon-maps";
import Link from "next/link"
import { useEffect, useState } from "react";
const Filter = require('bad-words')
const filter = new Filter();
import {database} from "../../../firebase-config"
import {remove, ref} from "firebase/database"
// Icons
import PersonIcon from '@mui/icons-material/Person';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import CircleIcon from '@mui/icons-material/Circle';
// Colors for Messages
const userColors = [
"#ff80ed",
"#065535",
"#133337",
"#ffc0cb",
"#e6e6fa",
"#ffd700",
"#ffa500",
"#0000ff",
"#1b85b8",
"#5a5255",
"#559e83",
"#ae5a41",
"#c3cb71",
];
"#ff80ed",
"#065535",
"#133337",
"#ffc0cb",
"#e6e6fa",
"#ffd700",
"#ffa500",
"#0000ff",
"#1b85b8",
"#5a5255",
"#559e83",
"#ae5a41",
"#c3cb71",
];
// Chat Message
export function Chat({ chatObj }) {
let dateOptions = {
weekday: "long",
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
};
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;
};
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)] }}>
{chatObj.user}
</span>
: {chatObj.body}
</div>
<div className="text-right text-[#d1d1d1]">
{new Date(chatObj.timestamp).toLocaleString(dateOptions)}
</div>
</div>
);
}
// System Chat Message
export function SystemMessage({ chatObj }) {
let dateOptions = {
weekday: "long",
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
weekday: "long",
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
};
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);
/**
* Photo URL Test
* @param {String} url - URL to Test
* @returns {Boolean} - Image Loaded (True) or Not (False)
*/
function imageProcessing(url) {
var x = async () => {
var img = new Image();
img.src = url;
img.onload = function() {
return true;
}
const index = Math.abs(hash) % userColors.length;
return index;
img.onerror = function() {
return false;
}
}
var res = x()
return res
}
/**
* Rich Message Formatting
* @param {String} message - Message to Format
* @returns {String} - Formatted Message (IN HTML)
*/
export 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);
var newMessage = URLmatch ? [] : message
if (URLmatch) {
for (var i = 0; i < URLmatch.length; i++) {
if (imageProcessing("https://"+URLmatch[i])) {
newMessage.push((<span className="mr-2">
{(URLmatch.length == 1) && message.split(URLmatch[i])[0].replace("https://","").replace("http://","")}
<img src={"https://"+URLmatch[i]} className="max-w-[100%]"/>
{(i == URLmatch.length || URLmatch.length == 1) && message.split(URLmatch[i])[1]}
</span>))
} else {
newMessage.push((<span className="mr-2">
{URLmatch.length == 1 && message.split(URLmatch[i])[0]}
<Link href={"https://"+URLmatch[i]} target="_blank" className="hover:underline">{URLmatch[i]}</Link>
{(i == URLmatch.length || URLmatch.length == 1) && message.split(URLmatch[i])[1]}
</span>))
}
}
}
return newMessage
}
/**
* 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;
};
return (
/**
* Chat Message Object
* @props {JSON} chatObj - Chat Object
* @returns {Object} - Chat Message Component
*/
export function Chat({ chatObj, user, path }) {
function deleteMessage() {
remove(ref(database, `${path}/chats/${chatObj.timestamp}-${chatObj.user}`))
}
var messageFilterBypass = [undefined, null, '', ' ', '\'', '\"']
if (!messageFilterBypass.includes(chatObj.body) && (chatObj.body.length != 1 && !chatObj.body[0].match(/\W/))) {
var message = filter.clean(chatObj.body)
message = RMF(message)
} else {
var message = chatObj.body
}
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]">
<div>
{user.uid == chatObj.uid && <DeleteOutlineIcon fontSize="" className="ml-1 mr-1 cursor-pointer" onClick={() => {deleteMessage()}}/>}
<span className="mr-[5px]" 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)] }}>
{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]">
</div>
<div className="text-right text-[#d1d1d1]">
{new Date(chatObj.timestamp).toLocaleString(dateOptions)}
</div>
</div>
</div>
);
);
}
// Member for Active/Room members in sidebar
/**
* Member Object for Sidebar
* @prop {JSON} memberObj - Member Object
* @returns {Object} - Member Component
*/
export function Member({ memberObj }) {
return (
return (
<Link href={"/user?uid=" + memberObj.uid}>
<div className="cursor-pointer g-[aliceblue] rounded-lg m-3 shadow-xl p-2">
{memberObj.username}
{memberObj.lastOnline == true && <CircleIcon className="text-lime-600 mr-1 relative top-[-1px]" fontSize="20px"/>}{memberObj.username}
</div>
);
}
</Link>
);
}
// Chat Room Object for myRooms and Nearby in sidebar
export function ChatRoomSidebar({ roomObj, click }) {
// TODO: Gross fix but it works
function clicker() {
click(roomObj);
}
return (
<div
onClick={clicker}
className="border-[black] border-1 shadow-lg p-2 m-2 rounded-lg cursor-pointer"
>
<div className="col-span-2">
<div className="font-bold">{roomObj.name}</div>
<div className="italic">{roomObj.description}</div>
</div>
</div>
);
}
/**
* Chat Room Object for Sidebar
* @prop {JSON} roomObj - Room Object
* @returns {Object} - Chat Room Component
*/
export function ChatRoomSidebar({ roomObj }) {
const [isRoomHovered, setIsRoomHovered] = useState(false);
// Map module for main page and chat room sidebar
// TODO: MAKE NOT MOVABLE
export function Geo({ loc, zoom, locMarker, markers }) {
if (loc) {
return (
<Map center={[loc.latitude, loc.longitude]} defaultZoom={zoom}>
{markers && markers}
{locMarker && (
<Marker
width={30}
anchor={[loc.latitude, loc.longitude]}
color="red"
/>
)}
{zoom && <ZoomControl />}
</Map>
);
if ("users" in roomObj) {
if ("online" in roomObj.users) {
var roomOnline = Object.keys(roomObj.users.online).length
} else {
return (
<Map className="rounded-lg" defaultCenter={[0, 0]} defaultZoom={zoom} />
);
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,59 @@
// Firebase Imports
import { database } from "../../../../firebase-config"
import { ref, set, get } from "firebase/database";
import ChatIcon from '@mui/icons-material/Chat';
import CircleIcon from '@mui/icons-material/Circle';
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='grid grid-cols-2 justify-items-center'>
<div className='mr-8'><img src={friendObj.pfp} className= 'w-[50px] h-[50px]'/></div>
<div className=''>
<div className="font-bold">{friendObj.lastOnline == true && <CircleIcon className="text-lime-600 mr-1 relative top-[-2px]" fontSize="20px"/>}{friendObj.firstName} {friendObj.lastName}</div>
<div className="">@{friendObj.username}</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,107 @@
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';
import CircleIcon from '@mui/icons-material/Circle';
/**
* 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='grid grid-cols-2 justify-items-center'>
<div className='mr-8'><img src={friendObj.pfp} className= 'w-[50px] h-[50px]'/></div>
<div className=''>
<div className="font-bold">{friendObj.lastOnline == true && <CircleIcon className="text-lime-600 mr-1 relative top-[-2px]" fontSize="20px"/>}{friendObj.firstName} {friendObj.lastName}</div>
<div className="">@{friendObj.username}</div>
</div>
</div>
</Link>
</div>
</div>
</div>
)
}
/**
*
* @prop {JSON} user - User Object
* @prop {JSON} requestingUser - User Object of the user requesting to be friends
* @returns {Object} Friend Request Component
*/
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,142 @@
// Dependency Imports
import { Form, useForm } from "react-hook-form";
// Firebase Imports
import { ref, set, onChildAdded, onChildRemoved } from "firebase/database";
import { database } from "../../../../firebase-config";
// Component Imports
import { Chat, SystemMessage } from "../datatypes";
// Icons
import SendIcon from '@mui/icons-material/Send';
// Notification
import { createNotification } from "../notifications/notifications";
import { useEffect, useState } from "react";
/**
* Chat Room Component
* @prop {JSON} roomObj - Room Object
* @prop {JSON} user - User Object
* @returns {Object} - Chat Room Component
*/
export function DMRoom({ roomObj, user }) {
const [chatRoomObj, setChatRoomObj] = useState(roomObj);
const [chats, setChats] = useState(null);
var { register, control, reset, handleSubmit } = useForm();
// Listeners for DMs
useEffect(() => {
var path = roomObj.UIDs[0] < roomObj.UIDs[1] ? roomObj.UIDs[0] + "-" + roomObj.UIDs[1] : roomObj.UIDs[1] + "-" + roomObj.UIDs[0];
onChildAdded(ref(database, `/dms/${path}/chats`), (newDM) => {
if (chatRoomObj) {
var newDMRoomObj = chatRoomObj
if (newDMRoomObj) {
if (!newDMRoomObj.chats) {
newDMRoomObj.chats = {}
}
newDMRoomObj.chats[newDM.key] = newDM.val()
setChatRoomObj({...newDMRoomObj})
}
}
});
onChildRemoved(ref(database, `/dms/${path}/chats`), (removed) => {
if (chatRoomObj) {
var newDMRoomObj = chatRoomObj
var deleted = removed.val()
delete newDMRoomObj.chats[`${deleted.timestamp}-${deleted.user}`]
setChatRoomObj({...newDMRoomObj})
}
});
}, [])
useEffect(() => {
// Message updater
var chatsArr = [];
var messages = chatRoomObj.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]}
user={user}
path={"/dms/" + chatRoomObj.room}
key={messages[message].timestamp}
/>
);
}
}
setChats(chatsArr.reverse())
}, [chatRoomObj])
/**
* Send Message in Chatroom
* @param {JSON} data - Message data to send (from form)
* @returns {void}
*/
function sendMessage(data) {
// Other UID
var otherUID = chatRoomObj.initUID == user.uid ? chatRoomObj.targetUID : chatRoomObj.initUID;
// Send other user notification if not in room
if (chatRoomObj.users && chatRoomObj.users.online) {
if (!(otherUID in chatRoomObj.users.online)) {
createNotification(
"New Message",
`${user.username} sent you a message.`,
"dm",
user.uid,
otherUID
);
}
}
var messageFilterBypass = [undefined, null, "", " ", ' ', '\'']
reset();
if (!messageFilterBypass.includes(data.message)) {
var payload = {
body: data.message,
user: user.username,
uid: user.uid,
isSystem: false,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/dms/${chatRoomObj.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>
);
}
+155 -122
View File
@@ -1,127 +1,160 @@
import { auth, database } from "../../app/api/firebase-config";
import { ref, set, remove } from "firebase/database";
import {signOut} from "firebase/auth";
function logout() {
console.log("Fire")
signOut(auth)
}
// System Imports
import Link from "next/link"
// Closes chat room
function closeChatRoom(roomObj, setChatRoomObj, setMainTab, user) {
var path = roomObj.path + "/" + roomObj.name + "-" + roomObj.timestamp;
var payload = {
body: "left",
user: user.username,
isSystem: true,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/rooms/${path}/chats/${new Date().getTime()}-${user.username}`
),
payload
);
remove(ref(database, `/rooms/${path}/users/online/${user.uid}`));
setChatRoomObj(null);
setMainTab("home");
}
// Firebase Imports
import { database } from "../../../firebase-config";
import { ref, set, remove, onDisconnect, serverTimestamp } from "firebase/database";
// Adds room to myRooms
function addToMyRooms(chatRoomObj, setIsMyRoom, 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);
setIsMyRoom(true);
}
// Component Imports
import { NotificationPanel } from "./notifications/notifications";
import { ProfilePanel } from "./profile/ProfilePanel"
// Deletes saved room from myRooms
function removeFromMyRooms(chatRoomObj, setIsMyRoom, 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}`));
setIsMyRoom(false);
}
// 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';
export function Header({mainTab, isMyRoom, chatRoomObj, setChatRoomObj, setMainTab, setIsMyRoom, user}) {
return (
<div className="m-2 rounded-lg h-[63px] bg-white shadow-2xl grid grid-cols-2 p-1">
<div className="h-[60px]">
<a href="/">
<img
src="logos/logo_transparent_inverse.png"
className="h-[60px]"
/>
</a>
</div>
<div className="h-[60px] p-4">
{mainTab == "chat" && isMyRoom == false && (
<a
onClick={() => {
addToMyRooms(chatRoomObj, setIsMyRoom, user);
}}
className="p-2 cursor-pointer bg-[#dee0e0] bg-cyan-500 text-white font-bold rounded-full mr-5"
>
Add to &quot;My Rooms&quot;
</a>
)}
{mainTab == "chat" && isMyRoom == true && (
<a
onClick={() => {
removeFromMyRooms(chatRoomObj, setIsMyRoom, user);
}}
className="p-2 cursor-pointer bg-[#dee0e0] bg-cyan-500 text-white font-bold rounded-full mr-5"
>
Remove from &quot;My Rooms&quot;
</a>
)}
{mainTab == "chat" && (
<a
onClick={() => {
closeChatRoom(chatRoomObj, setChatRoomObj, setMainTab, user);
}}
className="p-2 cursor-pointer bg-[#dee0e0] bg-cyan-500 text-white font-bold rounded-full mr-5"
>
Close Chat
</a>
)}
<a
onClick={logout}
href="/"
className="p-2 cursor-pointer bg-[#dee0e0] bg-cyan-500 text-white font-bold rounded-full"
>
Sign Out
</a>
</div>
</div>
/**
* 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;
}
}
// Sets User Online / Offline
// Stored in header for easy code maintenance and retains user online/offline throughout app
// Makes user online
if (user.invisibleStatus == false) {
set(ref(database, `/users/${user.uid}/lastOnline`), true)
}
// Makes user offline (with last time online)
onDisconnect(ref(database, `/users/${user.uid}/lastOnline`)).set(serverTimestamp())
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>
);
}
@@ -1,94 +0,0 @@
import { Chat, SystemMessage} from "../datatypes"
import { useState, useEffect } from "react";
import { Form, useForm } from "react-hook-form";
import { ref, onValue, set} from "firebase/database";
import { database } from "../../../app/api/firebase-config";
// Chatroom Module for Primary Tab
export function MainTabChatRoom({ roomObj, user }) {
var { register, control, reset, handleSubmit } = useForm();
const [chats, setData] = useState(null);
const [isLoading, setLoading] = useState(true);
// Message updater
useEffect(() => {
onValue(
ref(
database,
`/rooms/${
roomObj.path + "/" + roomObj.name + "-" + roomObj.timestamp
}/chats`
),
(snapshot) => {
var chatsArr = [];
var messages = snapshot.val();
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}
/>
);
}
}
setData(chatsArr.reverse());
setLoading(false);
}
);
});
function sendMessage(data) {
reset();
var payload = {
body: data.message,
user: user.username,
isSystem: false,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/rooms/${
roomObj.path + "/" + roomObj.name + "-" + roomObj.timestamp
}/chats/${new Date().getTime()}-${user.username}`
),
payload
);
}
if (isLoading) return <div>Loading</div>;
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}
className="w-[100%] p-[0px]"
>
<input
type="text"
{...register("message")}
placeholder="Enter message"
className="w-[83%] border-[0px] mt-[8px] mb-[8px]"
/>
<button className="p-2 cursor-pointer bg-[#dee0e0] bg-cyan-500 text-white font-bold rounded-full mr-5 w-[8%]">
Send
</button>
</Form>
</div>
</div>
);
}
@@ -1,31 +0,0 @@
import {Geo} from "../datatypes"
// Module for Welcome Message on main tab landing page
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
export function MainTabHome({ loc, markers, user }) {
return (
<>
<WelcomeMessage user={user}/>
<div className="h-[calc(100%-110px)] m-5 rounded-lg">
<Geo
loc={loc}
zoom={14}
movable={true}
locMarker={true}
markers={markers}
/>
</div>
</>
);
}
+137
View File
@@ -0,0 +1,137 @@
import { Map, Marker, ZoomControl, Overlay } from "pigeon-maps";
import { database } from "../../../../firebase-config";
import { ref, get} from "firebase/database";
import { useState } from "react";
import ChatBubbleTwoToneIcon from '@mui/icons-material/ChatBubbleTwoTone';
import PersonOutlineTwoToneIcon from '@mui/icons-material/PersonOutlineTwoTone';
import { red } from '@mui/material/colors';
/**
* Nearby Markers Grabber
* @param {JSON} location - Location Object {latitude, longitude}
* @returns {Array} - Array of Markers {<Marker>}
*/
function NearbyMarkers(location) {
const [newMarkers, setNewMarkers] = useState(null);
if (location) {
const path = String(location.latitude.toFixed(2)).replace(".", "") +"/" +String(location.longitude.toFixed(2)).replace(".", "") +"/";
get(ref(database, `/rooms/${path}`)).then((snapshot) => {
if (snapshot.exists()) {
const rooms = snapshot.val();
setNewMarkers(rooms)
}
})
}
return newMarkers;
}
/**
* Friend Markers Grabber
* @param {JSON} user - User Object
* @returns {Array} - Array of Markers {<Marker>}
*/
function FriendMarkers(user) {
var friendMarkers = []
if (user && "friends" in user && "friends" in user.friends) {
for (var friend in user.friends.friends) {
get(ref(database, `/users/${friend}`)).then((snapshot) => {
var friendData = snapshot.val();
if (friendData.location) {
friendMarkers.push(friendData);
}
});
}
}
}
/**
* 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, user }) {
const [hovering, setHovering] = useState(false);
const [hoverText, setHoverText] = useState("");
const [hoverAnchor, setHoverAnchor] = useState([null,null]);
if (moveable) {
if (user.rooms) {
// Load My Rooms Markers
var myRoomsMarkers = Object.values(user.rooms).map((roomObj) => {
return (<Marker
key={roomObj.path + "-" + roomObj.name}
anchor={[roomObj.latitude, roomObj.longitude]}
onClick={() => {window.location.href = "/chat?room=" + roomObj.path + "/" + roomObj.name + "-" + roomObj.timestamp;}}
style={{pointerEvents:'auto'} /* So stupid */}
onMouseOver={() => {setHoverText(roomObj.name);setHovering(true);setHoverAnchor([roomObj.latitude, roomObj.longitude])}}
onMouseOut={() => {setHovering(false)}}
>
<ChatBubbleTwoToneIcon color="primary" fontSize="large"/>
</Marker>)
})
}
// Load Nearby Markers
var nearbyMarkers = NearbyMarkers(loc);
if (nearbyMarkers) {
var nearbyMarkers = Object.values(nearbyMarkers).map((roomObj) => {
return (<Marker
key={roomObj.path + "-" + roomObj.name}
anchor={[roomObj.latitude, roomObj.longitude]}
onClick={() => {window.location.href = "/chat?room=" + roomObj.path + "/" + roomObj.name + "-" + roomObj.timestamp;}}
style={{pointerEvents:'auto'} /* So stupid */}
onMouseOver={() => {setHoverText(roomObj.name);setHovering(true);setHoverAnchor([roomObj.latitude, roomObj.longitude])}}
onMouseOut={() => {setHovering(false)}}
>
<ChatBubbleTwoToneIcon color="secondary" fontSize="large"/>
</Marker>)
})
}
}
if (!loc) {
return <div>Getting Location...</div>;
} else {
return (
<>
<Map
center={[loc.latitude, loc.longitude]}
defaultZoom={zoom}
mouseEvents={moveable}
touchEvents={moveable}
attribution={false}
>
{zoom && <ZoomControl />}
{moveable && nearbyMarkers}
{moveable && myRoomsMarkers}
{ /* Overlay */}
{hovering && (
<Overlay anchor={hoverAnchor} offset={[0, 0]}>
<div className="bg-white rounded-lg p-2">
<p className="text-lg">{hoverText}</p>
</div>
</Overlay>
)}
{user && ( // Shows the user marker
<Marker
anchor={[loc.latitude, loc.longitude]}
color="red"
style={{pointerEvents:'auto'} /* So stupid */}
>
<PersonOutlineTwoToneIcon sx={{ color: red[500] }} fontSize="large"/>
</Marker>
)}
</Map>
</>
);
}
}
@@ -0,0 +1,112 @@
// 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}) {
/**
* Removes Notification
* @returns {void}
*/
function removeNotification() {
remove(ref(database, `/users/${data.ruser}/notifications/${data.suser}-${data.action}`))
}
/**
* Determines Action
*/
function onClick() {
if (data.action === "dm") {
var order = data.suser > data.ruser ? data.ruser + "-" + data.suser : data.suser + "-" + data.ruser;
window.location.href = "/dm?dm=" + order;
removeNotification()
}
}
return (
<div className="hover:bg-[#C0C0C0] rounded-lg cursor-pointer" >
<div className="float-right top-0 cursor-pointer p-2 text-[24px] text-slate-500">
<div onClick={() => {removeNotification()}}><CloseIcon/></div>
</div>
<div className="p-3 text-left" onClick={() => {onClick()}}>
<span className="font-bold">{data.title}</span><br/>
{data.byline}<br/>
</div>
</div>
)
}
/**
* 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}
*/
export function createNotification(title, byline, action, suser, ruser) {
var timestamp = new Date().getTime();
var payload = {
title: title,
byline: byline,
action: action,
suser: suser,
ruser: ruser,
id: suser + "-" + action,
timestamp: timestamp
};
set(ref(database, `/users/${ruser}/notifications/${suser}-${action}`), payload);
}
/**
* Notification Panel
* @constructor
* @prop {user} user - User object (from Firebase)
* @returns {NotificationPanel} - Notification Panel Component
*/
export function NotificationPanel({user}) {
var notificationsMap = []
if (user.notifications) {
for (var notificationPackage in user.notifications) {
notificationsMap.push(<Notification data={user.notifications[notificationPackage]}/>)
}
} else {
notificationsMap = null
}
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">
{notificationsMap}
{!notificationsMap &&
<div className="h-[64px] flex flex-col justify-center items-center">
<NotificationsPausedIcon/> All caught up!
</div>
}
</div>
</Popover.Panel>
</Popover>
)
}
@@ -0,0 +1,121 @@
// Dependency Imports
import { Form, useForm } from "react-hook-form";
// Firebase Imports
import { ref, set, onChildAdded, onChildRemoved } from "firebase/database";
import { database } from "../../../../firebase-config";
// Component Imports
import { Chat, SystemMessage } from "../datatypes";
// Icons
import SendIcon from '@mui/icons-material/Send';
import { useState,useEffect } from "react";
/**
* Chat Room Component
* @prop {JSON} roomObj - Room Object
* @prop {JSON} user - User Object
* @returns {Object} - Chat Room Component
*/
export function ChatRoom({ roomObj, user }) {
const [chatRoomObj, setChatRoomObj] = useState(roomObj);
const [chats, setChats] = useState(null);
var { register, control, reset, handleSubmit } = useForm();
// Listeners for Chats
useEffect(() => {
var path = chatRoomObj.path + "/" + chatRoomObj.name + "-" + chatRoomObj.timestamp;
onChildAdded(ref(database, `/rooms/${path}/chats`), (newChat) => {
var newChatRoomObj = chatRoomObj
if (newChatRoomObj) {
if (!newChatRoomObj.chats) {
newChatRoomObj.chats = {}
}
newChatRoomObj.chats[newChat.key] = newChat.val()
setChatRoomObj({...newChatRoomObj})
}
});
onChildRemoved(ref(database, `/rooms/${path}/chats`), (removed) => {
if (chatRoomObj) {
var newChatRoomObj = chatRoomObj
var deleted = removed.val()
delete newChatRoomObj.chats[`${deleted.timestamp}-${deleted.user}`]
setChatRoomObj({...newChatRoomObj})
}
});
}, [])
useEffect(() => {
// Message updater
var chatsArr = [];
var messages = chatRoomObj.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]}
user={user}
path={"/rooms/" + chatRoomObj.path + "/" + chatRoomObj.name + "-" + chatRoomObj.timestamp}
key={messages[message].timestamp}
/>
);
}
}
setChats(chatsArr.reverse())
}, [chatRoomObj])
/**
* Send Message in Chatroom
* @param {JSON} data - Message data to send (from form)
* @returns {void}
*/
function sendMessage(data) {
var messageFilterBypass = [undefined, null, '', ' ', '\'', '\"']
if (!messageFilterBypass.includes(data.message)) {
reset();
var payload = {
body: data.message,
user: user.username,
uid: user.uid,
isSystem: false,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/rooms/${chatRoomObj.path + "/" + chatRoomObj.name + "-" + chatRoomObj.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,138 @@
// 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 profileData) {
if (data[key] == "" || (key == "pfp" && data[key].length == 0)) {
data[key] = profileData[key];
}
}
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}
style={{width: "150px", maxHeight: "400px"}}
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,70 @@
// System Imports
import { Popover } from "@headlessui/react";
import Link from "next/link"
// Firebase Imports
import { auth, database } from "../../../../firebase-config";
import { signOut } from "firebase/auth";
import {update, ref, serverTimestamp} from "firebase/database";
/**
* 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 flex items-center">
<img
src={user.pfp}
style={{maxWidth: "40px", maxHeight: "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>
<div className="rounded-xl p-4 hover:bg-[#C0C0C0] cursor-pointer"
onClick={() => {
// Toggle Invisible Status
update(ref(database, `/users/${user.uid}`), {
invisibleStatus: user.invisibleStatus? !user.invisibleStatus: true,
lastOnline: user.invisibleStatus? true: serverTimestamp()
}
);
}}
>
{user.invisibleStatus ? "Go Online" : "Go Invisible"}
</div>
<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>
);
}
@@ -1,38 +1,67 @@
import { Geo } from "../datatypes";
// Component Imports
import { Geo } from "../map/geo";
import { Member } from "../datatypes"
export function Chat_Sidebar({chatRoomObj, chatroomOnline, chatroomUsersLoading, chatroomUsers}) {
return (
<div className="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}
movable={false}
marker={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>
{!chatroomUsersLoading && chatroomUsers}
</div>
// 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, onValue} from "firebase/database"
import { useState, useEffect } from "react"
export function Sidebar({user, chatRoomObj}) {
const [profileData, setProfileData] = useState(null)
const [chatroomOnline, setChatroomOnline] = useState(null)
var path = chatRoomObj.UIDs[0] < chatRoomObj.UIDs[1] ? chatRoomObj.UIDs[0] + "-" + chatRoomObj.UIDs[1] : chatRoomObj.UIDs[1] + "-" + chatRoomObj.UIDs[0];
var activeUsers = []
onValue(ref(database, `/dms/${path}/users/online`), (snapshot) => {
if (snapshot.exists()) {
var activeUsersJSON = snapshot.val();
for (var activeUser in activeUsersJSON)
activeUsers.push(<Member memberObj={activeUsersJSON[activeUser]} key={activeUser} />);
}
})
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} style={{maxHeight: "800px"}} 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>
{activeUsers}
</div>
</div>
</div>
)}
</div>
);
}
+233 -117
View File
@@ -1,126 +1,242 @@
// System Imports
import { Form, useForm } from "react-hook-form";
import { database } from "../../../app/api/firebase-config";
import { ref, set } from "firebase/database";
import { useEffect, useState } from "react";
// CreateRoom Module for Sidebar Create Tab
// Dependency Imports
import { Tab } from '@headlessui/react'
// Firebase Imports
import { database } from "../../../../firebase-config";
import { ref, set, get } from "firebase/database";
// Component Imports
import { NearbySidebar } from "./nearby";
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();
var { register, control, reset, handleSubmit } = useForm();
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-[#dee0e0] bg-cyan-500 text-white font-bold rounded-full mr-5">
Create
</button>
</Form>
</div>
);
/**
* 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);
}
export function Home_Sidebar({tab, nearby, loadingNearby, setTab, isRoomLoading, myRooms, loadingLoc, location}) {
return (
<div className="h-dvh">
<div className="bg-white shadow-2xl rounded-lg m-2 h-[98%]">
<div className="p-2">
<div className="p-1 rounded-lg grid grid-cols-3 bg-white">
<div
className={
tab == "nearby"
? "select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0] bg-[#D3D3D3]"
: "select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0]"
}
onClick={() => {
setTab("nearby");
}}
>
Nearby
</div>
<div
className={
tab == "rooms"
? "select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0] bg-[#D3D3D3]"
: "select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0]"
}
onClick={() => {
setTab("rooms");
}}
>
My Rooms
</div>
<div
className={
tab == "create"
? "select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0] bg-[#D3D3D3]"
: "select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0]"
}
onClick={() => {
setTab("create");
}}
>
Create
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 [friends, setFriends] = useState([])
const [friendRequests, setFriendRequests] = useState(null)
const [dms, setDMs] = useState((<div>No DMs</div>))
const [myRoomArr, setMyRoomArr] = useState([])
useEffect(() => {
var myRoomArr = [];
// Add myRooms to Sidebar
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);
})
}
setMyRoomArr(myRoomArr)
}, [])
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}/>);
setDMs(dmArr);
})
} 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}/>);
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'
)}>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'
)}>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'
)}>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'
)}>Requests</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<div className="overflow-y-auto h-[90%]">
<div>
<NearbySidebar location={location}/>
</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 == "nearby" && (
<div className="overflow-y-auto h-[90%]">
<div>
{!nearby && !loadingNearby && (
<div>
No Nearby Rooms
<br />
Create One?
</div>
)}
{loadingNearby && <div>Loading...</div>}
{nearby}
</div>
</div>
)}
{tab == "rooms" && (
<div className="overflow-y-auto h-[90%]">
<div>
{isRoomLoading && <div>Loading</div>}
{!myRooms && !isRoomLoading && <div>No User Saved Rooms</div>}
{myRooms}
</div>
</div>
)}
{tab == "create" && !loadingLoc && <CreateRoom loc={location} />}
{tab == "create" && loadingLoc && <div>Loading...</div>}
</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>
);
}
@@ -0,0 +1,84 @@
import {useEffect, useState} from "react";
import {database} from "../../../../firebase-config";
import {get, ref} from "firebase/database";
import { useForm } from "react-hook-form"
import { ChatRoomSidebar } from "../datatypes";
export function NearbySidebar({location}) {
const [nearbyArr, setNearbyArr] = useState([])
const [displayedRooms, setDisplayedRooms] = useState([])
const [nearbyArrReady, setNearbyArrReady] = useState(false)
const {register, watch, setFocus} = useForm({defaultValues: {search: null}})
// Search Bar Value
const search = watch("search")
// Search Bar Component
function SearchBar() {
return (
<div className="w-[97%]">
<input type="text" placeholder="Search" {...register("search")} className="w-full p-2 border-2 border-gray-300 rounded-lg col-span-3" value={null} />
</div>
)
}
// Filters Rooms Based on Search
useEffect(() => {
if (search != "") {
var rooms = []
for (var nearbyRoom in nearbyArr) {
if (nearbyArr[nearbyRoom].props.roomObj.name.toLowerCase().includes(search.toLowerCase()) || nearbyArr[nearbyRoom].props.roomObj.description.toLowerCase().includes(search.toLowerCase())) {
rooms.push(<ChatRoomSidebar roomObj={nearbyArr[nearbyRoom].props.roomObj} key={nearbyArr[nearbyRoom].props.roomObj.timestamp}/>)
}
}
} else {
rooms = nearbyArr
}
setDisplayedRooms(rooms)
}, [search])
// Returns cursor to search bar on render
useEffect(() => {
setFocus("search")
}, [displayedRooms])
// Sets Initial Array of Nearby Rooms
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()
}
setNearbyArr(nearbyArr)
setDisplayedRooms(nearbyArr)
setNearbyArrReady(true)
})
}
}, [location])
return (
<div>
<SearchBar/>
{nearbyArrReady && displayedRooms}
{!nearbyArrReady && <div>Loading...</div>}
{nearbyArrReady && nearbyArr.length === 0 && <div className="pt-5">No Nearby Rooms<br />Create One?</div>}
</div>
)
}
@@ -1,7 +0,0 @@
export function Profile_Sidebar() {
return (
<div className="h-dvh">
<div className=" bg-white m-2 h-[98%]">Profile</div>
</div>
)
}
-1
View File
@@ -1,4 +1,3 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
+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

-6
View File
@@ -1,6 +0,0 @@
{
"name": "ChatMaps",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}