Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48cc4d2227 |
@@ -1,6 +1,3 @@
|
||||
# Android Build
|
||||
/frontend-next/android
|
||||
|
||||
# Firebase Stuff
|
||||
firebase-admin-key.json
|
||||
firebase*.json
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||

|
||||
Main repo for ChatMaps, our COS420 Project.
|
||||
|
||||
ChatMaps is a web-based social networking service that allows users to connect to others in their local geographic area. It implements an interactable mapping utility to show general user locations relative to 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.
|
||||
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.
|
||||
|
||||
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 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 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,19 +11,11 @@ The live version of this app can be found at:
|
||||
|
||||
https://chatma.ps/
|
||||
|
||||
Demo Login:<br/>
|
||||
Email: demo@chatma.ps<br/>
|
||||
Password: demoapp<br/>
|
||||
|
||||
Please note. The demo is restricted to sending messages, and creating rooms. This is intented to ensure the demo remains an adequate example of the functionalities of this application. If you would like to test the full set of features, please make an account.
|
||||
|
||||
|
||||
|
||||
A local version can be run (assuming you have Node installed) with:
|
||||
A local version can be run with:
|
||||
|
||||
cd frontend-next/
|
||||
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
npm run dev
|
||||
|
||||
@@ -31,5 +23,3 @@ then navigating to:
|
||||
|
||||
http://localhost:3000
|
||||
|
||||
NOTICE: Access to firebase from localhost has been disabled due to security considerations. To demo our app, please go to https://chatma.ps!
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
{
|
||||
"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",
|
||||
"no-console": 1,
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
"@next/next/no-img-element": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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();
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
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();
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
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();
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
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();
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
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();
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -1,54 +0,0 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -1,76 +0,0 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -1,27 +0,0 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -1,13 +0,0 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.jacsn.chatmaps',
|
||||
appName: 'ChatMaps',
|
||||
webDir: 'out',
|
||||
server: {
|
||||
androidScheme: 'https'
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,204 +0,0 @@
|
||||
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,4 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {output: 'export'};
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -5,44 +5,22 @@
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "npx serve@latest out",
|
||||
"lint": "next lint",
|
||||
"test": "jest"
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"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-firebase-hooks": "^5.1.1",
|
||||
"react-hook-form": "^7.50.1",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"tts-react": "^3.0.6"
|
||||
"react-hook-form": "^7.50.1"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<!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>
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 173 KiB |
@@ -1,7 +1,6 @@
|
||||
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",
|
||||
@@ -15,7 +14,6 @@ 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, storage };
|
||||
export { auth, app, database };
|
||||
@@ -10,8 +10,10 @@ export const metadata = {
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<main className={inter.className}>
|
||||
{children}
|
||||
</main>
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,118 +1,292 @@
|
||||
"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"
|
||||
|
||||
// Dependencies
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
// Refactored Component Imports
|
||||
// Data Structure Imports
|
||||
import { ChatRoomSidebar, Member } from "../../components/app/datatypes";
|
||||
|
||||
// Firebase Imports
|
||||
import { auth, database } from "../../../firebase-config";
|
||||
import { ref, onValue, set } from "firebase/database";
|
||||
import { useAuthState } from "react-firebase-hooks/auth"
|
||||
|
||||
// Component Imports
|
||||
// Header Import
|
||||
import { Header } from "../../components/app/header";
|
||||
import { HomePage } from "../../components/app/page/home";
|
||||
import { Sidebar } from "../../components/app/sidebar/home";
|
||||
import {useWindowSize} from "../../components/app/datatypes";
|
||||
|
||||
// Capacitor Import
|
||||
import { Geolocation } from '@capacitor/geolocation';
|
||||
// Main Tab Imports
|
||||
import { MainTabChatRoom } from "../../components/app/main_tab/chat";
|
||||
import { MainTabHome } from "../../components/app/main_tab/home";
|
||||
|
||||
/**
|
||||
* Contains most everything for the app homepage
|
||||
* @returns {Object} Home Page
|
||||
*/
|
||||
// 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
|
||||
function Home() {
|
||||
// It's time to document and change these awful variable names
|
||||
// State variables for app page
|
||||
const [user, setUser] = useState(null); // user data
|
||||
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 [loadingLoc, setLoadingLoc] = useState(true); // location variable loading, true = loading, false = finished loading
|
||||
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)
|
||||
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)
|
||||
|
||||
var windowSize = useWindowSize()
|
||||
// Authentication
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
Geolocation.getCurrentPosition().then((position) => {
|
||||
setCoords(position.coords);
|
||||
setLoadingLoc(false);
|
||||
});
|
||||
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"
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Saves users last loc to profile for friends map
|
||||
|
||||
// Grabs user data, saves to user, then lists the users saved rooms
|
||||
useEffect(() => {
|
||||
if (coords && user) {
|
||||
set(ref(database,`users/${user.uid}/location`), {
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude
|
||||
})
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [coords, user])
|
||||
},[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}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden h-dvh">
|
||||
{user && (
|
||||
<div className="overflow-hidden h-dvh">
|
||||
<div>
|
||||
{isAuthenticated && (
|
||||
<div className="grid grid-cols-4 auto-cols-max overflow-hidden">
|
||||
{/* Left Side of Page */}
|
||||
<div className="overflow-hidden h-dvh md:mr-[405px]">
|
||||
<div className="col-span-3 h-dvh">
|
||||
{/* Header */}
|
||||
<Header
|
||||
mainTab={"home"}
|
||||
user={user}
|
||||
sidebarControl={() => {setDrawerOpen(!drawerOpen)}}
|
||||
/>
|
||||
<Header mainTab={mainTab} isMyRoom={isMyRoom} chatRoomObj={chatRoomObj} setChatRoomObj={setChatRoomObj} setMainTab={setMainTab} setIsMyRoom={setIsMyRoom} user={user}/>
|
||||
{/* Main Page Section */}
|
||||
<div className="mr-2 h-[calc(100%-110px)]">
|
||||
{!loadingLoc && (
|
||||
<HomePage loc={coords} user={user} />
|
||||
{mainTab == "home" && !loadingLoc && (
|
||||
<MainTabHome loc={location} markers={markers} user={user}/>
|
||||
)}
|
||||
{loadingLoc && (
|
||||
<HomePage loc={null} user={user} />
|
||||
{mainTab == "home" && loadingLoc && (
|
||||
<MainTabHome loc={null} markers={markers} user={user}/>
|
||||
)}
|
||||
{mainTab == "chat" && <MainTabChatRoom 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 user={user} location={coords} loadingLoc={loadingLoc}/>
|
||||
</div>
|
||||
</Drawer>
|
||||
{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/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
export default Home;
|
||||
@@ -1,18 +0,0 @@
|
||||
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 (
|
||||
<main className={inter.className}>
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
"use client";
|
||||
// System Imports
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
// Firebase Imports
|
||||
import { auth, database } from "../../../firebase-config";
|
||||
import { ref, onValue, set, onDisconnect} from "firebase/database";
|
||||
import { useAuthState } from "react-firebase-hooks/auth"
|
||||
|
||||
// Component Imports
|
||||
import { Header } from "../../components/app/header";
|
||||
import { ChatRoom } from "../../components/app/page/chat";
|
||||
import { Sidebar } from "../../components/app/sidebar/chat";
|
||||
import {useWindowSize} from "../../components/app/datatypes";
|
||||
|
||||
|
||||
/**
|
||||
* Chat Page
|
||||
* @returns {Object} Chat Page
|
||||
*/
|
||||
function Chat() {
|
||||
// State variables for chat page
|
||||
const [user, setUser] = useState(null); // user data
|
||||
const [chatRoomObj, setChatRoomObj] = useState(null); // Current chatroom object
|
||||
const [doneLoading, setDoneLoading] = useState(false) // is the page done loading or not
|
||||
const [authUser, 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 && !user) {
|
||||
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
|
||||
onValue(ref(database, `/rooms/${path}`), (roomData) => {
|
||||
roomData = roomData.val();
|
||||
setChatRoomObj(roomData)
|
||||
if (!doneLoading) {
|
||||
setDoneLoading(true)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{(authUser && doneLoading) && (
|
||||
<div className="overflow-hidden h-dvh">
|
||||
{/* Left Side of Page */}
|
||||
<div className="overflow-hidden h-dvh md:mr-[400px]">
|
||||
{/* Header */}
|
||||
<Header
|
||||
mainTab={"chat"}
|
||||
chatRoomObj={chatRoomObj}
|
||||
user={user}
|
||||
sidebarControl={() => {setDrawerOpen(!drawerOpen)}}
|
||||
/>
|
||||
{/* Main Page Section */}
|
||||
<div className="mr-2 h-[calc(100%-110px)]">
|
||||
<ChatRoom roomObj={chatRoomObj} user={user} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Sidebar (Right Side of Page) */}
|
||||
<Drawer open={drawerOpen} anchor={"right"} variant={windowSize.width > 767? "persistent": "temporary"} onClose={() => {setDrawerOpen(false)}} sx={{
|
||||
width: windowSize.width > 767? 400: "80%",
|
||||
marginTop: 10,
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': {
|
||||
width: windowSize.width > 767? 400: "80%",
|
||||
borderLeft: 0,
|
||||
},
|
||||
}}>
|
||||
<div className="shadow-2xl">
|
||||
<Sidebar chatRoomObj={chatRoomObj}/>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Chat;
|
||||
@@ -1,18 +0,0 @@
|
||||
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 (
|
||||
<main className={inter.className}>
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
"use client";
|
||||
// System Imports
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
// Firebase Imports
|
||||
import { auth, database } from "../../../firebase-config";
|
||||
import { ref, onValue, set, onDisconnect} from "firebase/database";
|
||||
import { useAuthState } from "react-firebase-hooks/auth"
|
||||
|
||||
// Component Imports
|
||||
import { Header } from "../../components/app/header";
|
||||
import { DMRoom } from "../../components/app/friends/page";
|
||||
import { Sidebar } from "../../components/app/sidebar/dm";
|
||||
import {useWindowSize} from "../../components/app/datatypes";
|
||||
|
||||
|
||||
/**
|
||||
* DM Page
|
||||
* @returns {Object} Chat Page
|
||||
*/
|
||||
function Chat() {
|
||||
// State variables for chat page
|
||||
const [user, setUser] = useState(null); // user data
|
||||
const [chatRoomObj, setChatRoomObj] = useState(null); // Current chatroom object
|
||||
const [doneLoading, setDoneLoading] = useState(false) // is the page done loading or not
|
||||
const [authUser, 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
|
||||
onValue(ref(database, `/dms/${path}`), (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;
|
||||
@@ -5,7 +5,7 @@
|
||||
body {
|
||||
background-color: aliceblue;
|
||||
text-align: center;
|
||||
text-wrap: pretty;
|
||||
text-wrap:pretty;
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -15,6 +15,7 @@ button {
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15));
|
||||
}
|
||||
|
||||
button:hover {
|
||||
|
||||
@@ -5,13 +5,15 @@ 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ export const metadata = {
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<main className={inter.className}>
|
||||
{children}
|
||||
</main>
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,135 +1,66 @@
|
||||
"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 "../../../firebase-config";
|
||||
import {setPersistence,signInWithEmailAndPassword,indexedDBLocalPersistence } from "firebase/auth";
|
||||
// Firebase imports
|
||||
import {auth} from "../api/firebase-config";
|
||||
import { setPersistence, signInWithEmailAndPassword, browserSessionPersistence } from "firebase/auth";
|
||||
|
||||
/**
|
||||
* Login Page
|
||||
* @returns {Object} Login Page
|
||||
*/
|
||||
function Login() {
|
||||
var router = useRouter();
|
||||
var {register,control,setError,reset,formState: { errors, isSubmitting, isSubmitted },} = useForm();
|
||||
var router = useRouter();
|
||||
//var { register, handleSubmit } = useForm();
|
||||
var { register, control, setError, handleSubmit, formState: { errors, isSubmitting, isSubmitted } } = useForm()
|
||||
|
||||
/**
|
||||
* Logs into Firebase authentication
|
||||
* @param {JSON} data - User Login Data (data.email, data.password)
|
||||
* @returns {void}
|
||||
*/
|
||||
function authenticate({data}) {
|
||||
setPersistence(auth, indexedDBLocalPersistence).then(() => {
|
||||
signInWithEmailAndPassword(auth, data.email, data.password).then(
|
||||
(userCredential) => {
|
||||
if (userCredential.user) {
|
||||
router.push("/app");
|
||||
}
|
||||
}
|
||||
).catch((error) => {
|
||||
if (error == "auth/invalid-credential") {
|
||||
const formError = {
|
||||
type: "server",
|
||||
message: "Username or Password Incorrect",
|
||||
};
|
||||
// set same error in both:
|
||||
setError("password", formError);
|
||||
setError("email", formError);
|
||||
reset(data,{keepErrors: true})
|
||||
} else {
|
||||
const formError = {
|
||||
type: "server",
|
||||
message: "Server Error, Please try again later.",
|
||||
};
|
||||
// set same error in both:
|
||||
setError("password", formError);
|
||||
setError("email", formError);
|
||||
reset(data,{keepErrors: true})
|
||||
}
|
||||
// ..
|
||||
});
|
||||
});
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid h-screen place-items-center">
|
||||
return (
|
||||
<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
|
||||
className="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
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="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 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Login;
|
||||
export default Login;
|
||||
@@ -5,14 +5,15 @@ 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 (
|
||||
<main className={inter.className}>
|
||||
{children}
|
||||
</main>
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,85 +1,58 @@
|
||||
"use client";
|
||||
// System Imports
|
||||
import "../globals.css";
|
||||
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, storage } from "../../../firebase-config";
|
||||
import { onAuthStateChanged } from "firebase/auth";
|
||||
import { ref as sRef, getDownloadURL } from "firebase/storage";
|
||||
import {auth, database} from "../api/firebase-config"
|
||||
import {onAuthStateChanged} from "firebase/auth"
|
||||
|
||||
/**
|
||||
* Onboarding Page
|
||||
* @returns {Object} - Onboarding Page
|
||||
*/
|
||||
function Onboarding() {
|
||||
var router = useRouter();
|
||||
var { register, handleSubmit } = useForm();
|
||||
|
||||
function Onboard(data) {
|
||||
function createUser(data) {
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
if (user.uid) {
|
||||
data.uid = user.uid;
|
||||
data.defined = true;
|
||||
data.invisibleStatus = false;
|
||||
data.bio = " ";
|
||||
data.interests = " , , "
|
||||
getDownloadURL(sRef(storage, `/default.png`)).then((url) => {
|
||||
data.pfp = url;
|
||||
data.email = user.email;
|
||||
set(ref(database, `users/${user.uid}`), data);
|
||||
router.push("/app");
|
||||
})
|
||||
|
||||
} 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 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>
|
||||
);
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default Onboarding;
|
||||
|
||||
|
||||
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 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;
|
||||
@@ -1,111 +1,74 @@
|
||||
"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";
|
||||
"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"
|
||||
|
||||
// Capacitor Import
|
||||
import { Geolocation } from '@capacitor/geolocation';
|
||||
|
||||
/**
|
||||
* Home Page
|
||||
* @returns {Object} - Home Page
|
||||
*/
|
||||
function Home() {
|
||||
const [isLoadingLoc, setLoadingLoc] = useState(true); // is location loading?
|
||||
const [roomCount, setRoomCount] = useState(null); // local room count
|
||||
const [isAuthenticated, setAuth] = useState(false); // is user authenticated?
|
||||
const [coords, setCoords] = useState(null)
|
||||
const [isLoadingLoc, setLoadingLoc] = useState(true)
|
||||
const [roomCount, setRoomCount] = useState(null)
|
||||
const [isAuthenticated, setAuth] = useState(false)
|
||||
const [userID, setUserID] = useState(null)
|
||||
|
||||
// Authentication
|
||||
// Authentication
|
||||
useEffect(() => {
|
||||
onAuthStateChanged(auth, async (user) => {
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
if (user) {
|
||||
setAuth(true);
|
||||
setUserID(user.uid)
|
||||
setAuth(true)
|
||||
} else {
|
||||
setAuth(false);
|
||||
setAuth(false)
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
Geolocation.getCurrentPosition().then((position) => {
|
||||
setCoords(position.coords);
|
||||
setLoadingLoc(false);
|
||||
});
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Update room count on location fix
|
||||
useEffect(() => {
|
||||
if (coords) {
|
||||
var path =
|
||||
String(coords.latitude.toFixed(2)).replace(".", "") +
|
||||
"/" +
|
||||
String(coords.longitude.toFixed(2)).replace(".", "");
|
||||
get(ref(database, `/rooms/${path}`)).then((snapshot) => {
|
||||
if (snapshot.exists()) {
|
||||
var count = 0;
|
||||
snapshot.forEach(() => {
|
||||
count++;
|
||||
});
|
||||
setRoomCount(count);
|
||||
} else {
|
||||
setRoomCount(0);
|
||||
}
|
||||
setLoadingLoc(false);
|
||||
});
|
||||
}
|
||||
}, [coords])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid h-screen place-items-center">
|
||||
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)
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
return (
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Home;
|
||||
export default Home;
|
||||
@@ -10,8 +10,10 @@ export const metadata = {
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<main className={inter.className}>
|
||||
{children}
|
||||
</main>
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,147 +1,78 @@
|
||||
"use client";
|
||||
// System Imports
|
||||
import "../globals.css";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm, Form } from "react-hook-form";
|
||||
import Link from "next/link"
|
||||
import "../globals.css"
|
||||
import { useState } from "react";
|
||||
|
||||
// Firebase Imports
|
||||
import {createUserWithEmailAndPassword,signInWithEmailAndPassword,setPersistence,indexedDBLocalPersistence,} from "firebase/auth";
|
||||
import { auth } from "../../../firebase-config";
|
||||
import { createUserWithEmailAndPassword, signInWithEmailAndPassword, setPersistence, browserSessionPersistence } from "firebase/auth";
|
||||
import {auth} from "../api/firebase-config";
|
||||
|
||||
/**
|
||||
* Signs up user in Firebase Authentication
|
||||
* @param {JSON} data - User signup data (data.email, data.password)
|
||||
* @returns {Boolean} - True if user is signed up, False if user is not signed up
|
||||
* @async
|
||||
*/
|
||||
async function Signup(data) {
|
||||
var userCredential = await createUserWithEmailAndPassword(
|
||||
auth,
|
||||
data.email,
|
||||
data.password
|
||||
);
|
||||
if (userCredential.user) {
|
||||
setPersistence(auth, indexedDBLocalPersistence).then(() => {
|
||||
signInWithEmailAndPassword(auth, data.email, data.password).then(
|
||||
() => {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Register Page
|
||||
* @returns {Object} - Registration Page
|
||||
*/
|
||||
function Register() {
|
||||
var router = useRouter();
|
||||
var {
|
||||
register,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm();
|
||||
var emailRegex =
|
||||
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
|
||||
var [passwordMismatch, setPasswordMismatch] = useState(false);
|
||||
var [passwordLength, setPasswordLength] = useState(false);
|
||||
const passwordMatch = (data) => {
|
||||
return data.password === data.passwordCheck;
|
||||
};
|
||||
|
||||
const passwordLengthMatch = (data) => {
|
||||
return data.password.length >= 6;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form onSubmit Handler
|
||||
* @params {JSON} data - Form data
|
||||
*/
|
||||
function onSubmit({ data }) {
|
||||
if (passwordMatch(data) && passwordLengthMatch(data)) {
|
||||
setPasswordMismatch(false);
|
||||
setPasswordLength(false)
|
||||
if (Signup(data)) {
|
||||
router.push("/onboarding");
|
||||
}
|
||||
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 {
|
||||
if (!passwordMatch(data)) {
|
||||
setPasswordMismatch(true);
|
||||
}
|
||||
if (!passwordLengthMatch(data)) {
|
||||
setPasswordLength(true);
|
||||
}
|
||||
return;
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid h-screen place-items-center">
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Register;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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 (
|
||||
<main className={inter.className}>
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
"use client";
|
||||
// System Imports
|
||||
import { useState, useEffect } from "react";
|
||||
import { auth, database } from "../../../firebase-config";
|
||||
import { ref, onValue } 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]} key={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]} key={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} mainTab={"profile"} />
|
||||
{/* 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 max-h-[20%] max-w-[70%]"
|
||||
/>
|
||||
<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;
|
||||
@@ -1,305 +1,142 @@
|
||||
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';
|
||||
|
||||
import { TextToSpeech } from 'tts-react'
|
||||
|
||||
// Chat Commands Dictionary
|
||||
const chatCommands = {
|
||||
"/peter": "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExcThtYjMxcWk2NG55YTE3OHdvNWJwcXVrZzV1ZmZkdWJ6cHBremFwdiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/13w5HmyiuaZ224/giphy.gif",
|
||||
"/shrug": "¯\\_(ツ)_/¯",
|
||||
"/tableflip": "(╯°□°)╯︵ ┻━┻",
|
||||
"/unflip": "┬─┬ ノ( ゜-゜ノ)",
|
||||
"/snoop": "https://media1.tenor.com/m/cIUjlvgnFRgAAAAd/dog-snoop.gif"
|
||||
}
|
||||
import { Map, Marker, ZoomControl } from "pigeon-maps";
|
||||
|
||||
// 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",
|
||||
];
|
||||
|
||||
let dateOptions = {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
/**
|
||||
* Photo URL Test
|
||||
* @param {String} url - URL to Test
|
||||
* @returns {Boolean} - Image Loaded (True) or Not (False)
|
||||
*/
|
||||
function imageProcessing(url) {
|
||||
var x = new Promise((resolve) => {
|
||||
var img = new Image();
|
||||
img.src = url;
|
||||
img.onload = () => {
|
||||
resolve([true, url]);
|
||||
}
|
||||
img.onerror = () => {
|
||||
resolve([false, url]);
|
||||
}
|
||||
})
|
||||
return x
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Rich Message Formatting
|
||||
* @param {String} message - Message to Format
|
||||
* @returns {String} - Formatted Message (IN HTML)
|
||||
*/
|
||||
export async function RMF(message) {
|
||||
var x = new Promise(async (resolve) => {
|
||||
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 (URLmatch[i].includes("chatma.ps") ) {
|
||||
// Rich Message Formatting for Chat Maps
|
||||
if (URLmatch[i].includes("/chat?")) {
|
||||
var roomName = URLmatch[i].split("?")[1].split("/")[2].split("-")[0].replaceAll("%20"," ")
|
||||
newMessage.push((<span className="italic" key={roomName}>invites you to <Link href={"https://"+URLmatch[i]} className="underline" target="_blank">{roomName}</Link></span>))
|
||||
}
|
||||
} else {
|
||||
await imageProcessing("https://"+URLmatch[i]).then((result) => {
|
||||
if (result[0]) {
|
||||
newMessage.push((<span className="mr-2" key={URLmatch[i]}>
|
||||
{(URLmatch.length == 1) && message.split(result[1])[0].replace("https://","").replace("http://","")}
|
||||
<img src={result[1]} className="max-w-[100%]"/>
|
||||
{(i == URLmatch.length || URLmatch.length == 1) && message.split(result[1])[1]}
|
||||
</span>))
|
||||
} else {
|
||||
newMessage.push((<span className="mr-2" key={URLmatch[i]}>
|
||||
{URLmatch.length == 1 && message.split(URLmatch[i])[0]}
|
||||
<Link href={result[1]} target="_blank" className="hover:underline">{URLmatch[i]}</Link>
|
||||
{(i == URLmatch.length || URLmatch.length == 1) && message.split(URLmatch[i])[1]}
|
||||
</span>))
|
||||
}
|
||||
})
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
resolve(newMessage)
|
||||
|
||||
});
|
||||
return x
|
||||
}
|
||||
/**
|
||||
* Grabs Window Size
|
||||
* @returns {Object} - Window Size Object (width, height)
|
||||
*/
|
||||
export function useWindowSize() {
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize();
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
return windowSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates Color based on string hash
|
||||
* @param {String} user_str - Username / String for hashing
|
||||
* @returns {String} - Color Hex Code Index in userColors
|
||||
*/
|
||||
const generateColor = (user_str) => {
|
||||
// hashes username for consistent colors, maybe all functionality to pick color later
|
||||
let hash = 0;
|
||||
for (let i = 0; i < user_str.length; i++) {
|
||||
hash = user_str.charCodeAt(i) + (hash * 32 - hash);
|
||||
}
|
||||
const index = Math.abs(hash) % userColors.length;
|
||||
return index;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Chat Message Object
|
||||
* @props {JSON} chatObj - Chat Object
|
||||
* @returns {Object} - Chat Message Component
|
||||
*/
|
||||
export function Chat({ chatObj, user, path }) {
|
||||
const [message, setMessage] = useState([])
|
||||
function deleteMessage() {
|
||||
remove(ref(database, `${path}/chats/${chatObj.timestamp}-${chatObj.user}`))
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
if (chatObj.body in chatCommands) {
|
||||
chatObj.body = chatCommands[chatObj.body]
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Peter Griffin Easter Egg and others
|
||||
var messageFilterBypass = [undefined, null, '', ' ', '\'', '\"']
|
||||
if (!messageFilterBypass.includes(chatObj.body) && (chatObj.body.length != 1 && !chatObj.body[0].match(/\W/))) {
|
||||
var settingMessage = filter.clean(chatObj.body)
|
||||
RMF(settingMessage).then((result) => {
|
||||
setMessage(result)
|
||||
})
|
||||
} else {
|
||||
setMessage(chatObj.body)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{message && (
|
||||
<div className="width-[100%] bg-white rounded-lg mt-1 text-left p-1 grid grid-cols-2 mr-2">
|
||||
<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>
|
||||
{(typeof message == 'string' && message.substring(0,4) == "/tts")? (<TextToSpeech autoPlay={true}> {chatObj.user + " said " + filter.clean(message.substring(5,message.length))} </TextToSpeech>): message}
|
||||
</div>
|
||||
<div className="text-right text-[#d1d1d1]">
|
||||
{new Date(chatObj.timestamp).toLocaleString(dateOptions)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* System Chat Message Object
|
||||
* @prop {JSON} chatObj - Chat Object
|
||||
* @returns {Object} - System Chat Message Component
|
||||
*/
|
||||
// System Chat Message
|
||||
export function SystemMessage({ chatObj }) {
|
||||
return (
|
||||
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 className="text-[#d1d1d1]">
|
||||
<div className="text-[#d1d1d1]">
|
||||
<span style={{ color: userColors[generateColor(chatObj.user)] }}>
|
||||
<Link href={`/user?uid=${chatObj.uid}`}
|
||||
className="hover:font-bold cursor-pointer">
|
||||
{chatObj.user}
|
||||
</Link>
|
||||
{chatObj.user}
|
||||
</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 Object for Sidebar
|
||||
* @prop {JSON} memberObj - Member Object
|
||||
* @returns {Object} - Member Component
|
||||
*/
|
||||
|
||||
// Member for Active/Room members in sidebar
|
||||
export function Member({ memberObj }) {
|
||||
return (
|
||||
<Link href={"/user?uid=" + memberObj.uid}>
|
||||
return (
|
||||
<div className="cursor-pointer g-[aliceblue] rounded-lg m-3 shadow-xl p-2">
|
||||
{memberObj.lastOnline == true && <CircleIcon className="text-lime-600 mr-1 relative top-[-1px]" fontSize="20px"/>}{memberObj.username}
|
||||
{memberObj.username}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat Room Object for Sidebar
|
||||
* @prop {JSON} roomObj - Room Object
|
||||
* @returns {Object} - Chat Room Component
|
||||
*/
|
||||
export function ChatRoomSidebar({ roomObj }) {
|
||||
const [isRoomHovered, setIsRoomHovered] = useState(false);
|
||||
|
||||
if ("users" in roomObj) {
|
||||
if ("online" in roomObj.users) {
|
||||
var roomOnline = Object.keys(roomObj.users.online).length
|
||||
} else {
|
||||
var roomOnline = 0
|
||||
}
|
||||
if ("all" in roomObj.users) {
|
||||
var roomTotal = Object.keys(roomObj.users.all).length
|
||||
} else {
|
||||
var roomTotal = 0
|
||||
}
|
||||
} else {
|
||||
var roomOnline = 0
|
||||
var roomTotal = 0
|
||||
);
|
||||
}
|
||||
const handleMouseEnter = () => {
|
||||
setIsRoomHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsRoomHovered(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-[black] border-1 shadow-lg p-2 m-2 rounded-lg cursor-pointer">
|
||||
<Link href={`/chat?room=${roomObj.path}/${roomObj.name}-${roomObj.timestamp}`}>
|
||||
<div className="grid grid-cols-3">
|
||||
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<PersonIcon/>
|
||||
{isRoomHovered && roomObj.users && (
|
||||
<div
|
||||
className="fixed bg-white p-2 shadow-md"
|
||||
>
|
||||
<ul>
|
||||
{Object.values(roomObj.users.all).map((user, index) => ( // I hate making lists like this
|
||||
<li key={index}>{user.username}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div>{roomOnline} / {roomTotal}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="font-bold">{roomObj.name}</div>
|
||||
<div className="italic">{roomObj.description}</div>
|
||||
</div>
|
||||
// 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>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// This will be removed once dateOptions is no longer used in this file
|
||||
export { dateOptions };
|
||||
// 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>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Map className="rounded-lg" defaultCenter={[0, 0]} defaultZoom={zoom} />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
// 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 + "-" + messages[message].user}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,162 +1,127 @@
|
||||
// System Imports
|
||||
import Link from "next/link"
|
||||
|
||||
// Firebase Imports
|
||||
import { database } from "../../../firebase-config";
|
||||
import { ref, set, remove, onDisconnect, serverTimestamp } from "firebase/database";
|
||||
|
||||
// Component Imports
|
||||
import { NotificationPanel } from "./notifications/notifications";
|
||||
import { ProfilePanel } from "./profile/ProfilePanel"
|
||||
|
||||
// Icons
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import RemoveIcon from '@mui/icons-material/Remove';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Closes Chat
|
||||
* @param {JSON} chatRoomObj - Chat Room Object
|
||||
* @param {JSON} user - User Object
|
||||
* @returns {void}
|
||||
*/
|
||||
function closeChat(chatRoomObj, user, mainTab) {
|
||||
if (mainTab == "chat") {
|
||||
remove(ref(database, `/rooms/${chatRoomObj.path}/${chatRoomObj.name}-${chatRoomObj.timestamp}/users/online/${user.uid}`))
|
||||
} else {
|
||||
remove(ref(database, `/dms/${chatRoomObj.room}/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;
|
||||
user.lastOnline = serverTimestamp();
|
||||
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}) {
|
||||
import { auth, database } from "../../app/api/firebase-config";
|
||||
import { ref, set, remove } from "firebase/database";
|
||||
import {signOut} from "firebase/auth";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function logout() {
|
||||
console.log("Fire")
|
||||
signOut(auth)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// 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)
|
||||
}
|
||||
}, [])
|
||||
// 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");
|
||||
}
|
||||
|
||||
// Makes user offline (with last time online)
|
||||
onDisconnect(ref(database, `/users/${user.uid}/lastOnline`)).set(serverTimestamp())
|
||||
// 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);
|
||||
}
|
||||
|
||||
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, mainTab)}}
|
||||
>
|
||||
<CloseIcon/>
|
||||
</Link>
|
||||
)}
|
||||
// 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);
|
||||
}
|
||||
|
||||
{/* Notifications Panel */}
|
||||
<NotificationPanel user={user}/>
|
||||
|
||||
{/*Profile Dropdown */}
|
||||
<ProfilePanel user={user}/>
|
||||
|
||||
{/* Sidebar Control (for small screens) */}
|
||||
{mainTab !== "profile" && (<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>)}
|
||||
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 "My Rooms"
|
||||
</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 "My Rooms"
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { Map, Marker, ZoomControl, Overlay } from "pigeon-maps";
|
||||
import { database } from "../../../../firebase-config";
|
||||
import { ref, get} from "firebase/database";
|
||||
import { useEffect, useState } from "react";
|
||||
import ChatBubbleTwoToneIcon from '@mui/icons-material/ChatBubbleTwoTone';
|
||||
import PersonOutlineTwoToneIcon from '@mui/icons-material/PersonOutlineTwoTone';
|
||||
import { red } from '@mui/material/colors';
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
const [nearbyMarkersFinal, setNearbyMarkers] = useState(null);
|
||||
|
||||
if (moveable && 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>)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Load Nearby Markers
|
||||
if (moveable && loc) {
|
||||
const path = String(loc.latitude.toFixed(2)).replace(".", "") +"/" +String(loc.longitude.toFixed(2)).replace(".", "") +"/";
|
||||
get(ref(database, `/rooms/${path}`)).then((snapshot) => {
|
||||
console.log("ran")
|
||||
if (snapshot.exists()) {
|
||||
nearbyMarkers = snapshot.val();
|
||||
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>)
|
||||
})
|
||||
setNearbyMarkers(nearbyMarkers);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
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 && nearbyMarkersFinal}
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
// 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 + messages[message].user}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
chatsArr.push(
|
||||
<Chat
|
||||
chatObj={messages[message]}
|
||||
user={user}
|
||||
path={"/rooms/" + chatRoomObj.path + "/" + chatRoomObj.name + "-" + chatRoomObj.timestamp}
|
||||
key={messages[message].timestamp + "-" + messages[message].user}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// 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,67 +1,38 @@
|
||||
// Component Imports
|
||||
import { Geo } from "../map/geo";
|
||||
import { Member } from "../datatypes"
|
||||
import { Geo } from "../datatypes";
|
||||
|
||||
// Sidebar when in a Chatrooms
|
||||
/**
|
||||
* Sidebar while in Chatroom
|
||||
* @prop {JSON} chatRoomObj - Chatroom Object
|
||||
* @returns {Object} - Sidebar Component
|
||||
*/
|
||||
export function Sidebar({chatRoomObj}) {
|
||||
// Active users list
|
||||
if (
|
||||
chatRoomObj.hasOwnProperty("users") &&
|
||||
chatRoomObj.users.hasOwnProperty("online")
|
||||
) {
|
||||
var activeUsers = [];
|
||||
var activeUsersJSON = chatRoomObj.users.online;
|
||||
for (var user in activeUsersJSON)
|
||||
activeUsers.push(<Member memberObj={activeUsersJSON[user]} key={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]} key={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}
|
||||
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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||