67 Commits

Author SHA1 Message Date
Nicholas Pease e8dfc7bf88 Fix Homepage Buttons Loading Slow (#31) 2024-03-02 21:23:07 -05:00
Nicholas Pease 72bcccfe2f Add Preliminary Chatroom Support (#32)
Add Preliminary Chatroom Support
2024-03-02 21:22:53 -05:00
Sgoodridge96 2c457ab531 Fixed bugs from last PR: Updated register page for password confirmation (#30) 2024-02-27 15:27:54 -05:00
Nicholas Pease 1120f5eb6e Merge branch 'main' into sgoodridge-edit1 2024-02-26 21:12:43 -05:00
Nicholas Pease f16b77c94b Merge branch 'main' into npease-homepage-fix 2024-02-26 18:34:29 -05:00
Nicholas Pease 62a813d0ad Add Username to Onboarding, Fix Onboarding (#28) 2024-02-26 14:59:27 -05:00
Stephen 151de4ebfd updated register page for password confirmation: Fixed bugs with last PR 2024-02-26 14:22:04 -05:00
Sgoodridge96 199c283b0e Update page.js
Updated errors from pull request:
Added a password confirmation to register page
2024-02-26 13:37:34 -05:00
npease 60bdf36274 Chatrooms: Nearby Lookup Working 2024-02-26 05:40:11 +00:00
npease a7eb9b942b Minimal Functional Chatrooms 2024-02-25 23:46:41 +00:00
npease 7cd1e9b5da WIP: Message Fetch Works 2024-02-25 08:38:40 +00:00
npease afd5dbaa9f Restructure App, Created Chat UI, Ready for Firebase Integration / Backend 2024-02-25 03:19:12 +00:00
npease eeb6b856e6 UI Changes, Prepare for Chatrooms 2024-02-24 06:35:23 +00:00
npease 9ee8bf3376 Add username, fix onboarding 2024-02-24 02:15:53 +00:00
npease 848d588bf4 Fix Homepage Buttons Loading Slow 2024-02-23 20:02:08 +00:00
Stephen 7ca4b62848 Added password confirmation to register page 2024-02-23 14:05:14 -05:00
Stephen 8cd5fd8783 Added password confimation during registration 2024-02-23 14:02:34 -05:00
Stephen afd72ec72b Added password confirmation on register page 2024-02-23 13:52:38 -05:00
Nicholas Pease 0895e93f6c Optimize Authentication Flow, Better Visual Feedback (#25) 2024-02-23 12:09:52 -05:00
Stephen 67ec566728 Added re enter password to register page 2024-02-23 11:58:09 -05:00
npease 8737d10a1e If user is authenticated, redirect to app from /login and /register pages 2024-02-23 04:37:03 +00:00
npease d9bca7f1ff Loading Icon on Login/Register button press 2024-02-23 04:22:36 +00:00
npease 4b9b46f10d Improved Onboarding User Verification, Removed Login/Register Buttons on Homepage for Logged In Users 2024-02-23 04:09:45 +00:00
npease bafcd88fa1 Optimize User Info Storage & Reduce API Calls to DB 2024-02-23 03:44:05 +00:00
Nicholas Pease ac7317a0b7 Validation on Register / Remove Download Button From Homepage (#23) 2024-02-22 14:08:14 -05:00
Nicholas Pease 034b217916 Update README.md with new URL (#21)
Update README.md with new URL
2024-02-22 14:06:53 -05:00
Nicholas Pease ef3f7fa174 Login: Invalid Username / Password Prompts User (#22) 2024-02-22 14:06:31 -05:00
npease 17d2ce436a Delete Download Button 2024-02-22 19:00:47 +00:00
npease 6dbc0a2e8e Register Page Validation 2024-02-22 18:59:57 +00:00
npease 43e9045b0a Final CSS Changes 2024-02-22 18:24:25 +00:00
npease 5ca9c4222c Slight CSS changes to keep login page from scrolling 2024-02-22 18:22:55 +00:00
npease 5e8a5c89b3 Add red border on error to login 2024-02-22 18:18:59 +00:00
Nicholas Pease dc5469fc70 Update README.md with even newer URL 2024-02-22 12:09:38 -05:00
npease ac19919c51 Login: Invalid Username / Password Prompts User 2024-02-22 06:17:53 +00:00
Sgoodridge96 7d2b653953 Change button Colors (#20) 2024-02-21 09:29:54 -05:00
Nicholas Pease 6dea6cc168 Update README.md with new URL 2024-02-21 09:14:05 -05:00
Nicholas Pease 75e3476d48 Load default map then center on users computed location (#16) 2024-02-21 09:08:53 -05:00
Stephen c73f85a411 Merge branch 'main' of https://github.com/ChatMaps/ChatMaps into sgoodridge 2024-02-20 23:33:18 -05:00
Nicholas Pease 5aba1549d4 Deployment Fixes (#19) 2024-02-20 22:56:49 -05:00
npease 3f68e43efd Merge branch 'main' of https://github.com/LAX18/ChatMaps 2024-02-20 22:55:22 -05:00
npease b24a2b5254 Remove Logging 2024-02-20 22:55:20 -05:00
Nicholas Pease b38f12a6ab Merge branch 'ChatMaps:main' into main 2024-02-20 22:54:51 -05:00
npease b07a333459 Cookie Deployment Fix 2024-02-20 22:51:09 -05:00
Stephen 13e5f319c7 changes to buttons 2024-02-20 22:17:35 -05:00
npease 4391a072cc Different way of setting cookies 2024-02-20 17:33:06 -05:00
npease 66c9de922d New way of setting cookies 2024-02-20 17:27:01 -05:00
npease 245ae616b1 Testing fixes with cookie on middleware 2024-02-20 17:05:01 -05:00
Nicholas Pease 3afbe17a21 Fix Deployment Authentication Problems (#18) 2024-02-20 16:49:23 -05:00
Nicholas Pease 8d3283ef04 Merge branch 'ChatMaps:main' into main 2024-02-20 16:48:13 -05:00
npease 8862f7b94c Fix login on deployment 2024-02-20 16:47:51 -05:00
Nicholas Pease 6e5af78586 Merge branch 'main' into npease-ui-maps-fix 2024-02-20 16:39:24 -05:00
Nicholas Pease ca731147f2 Move Firebase Auth to .env files to support Vercel deployments (#17) 2024-02-20 16:33:12 -05:00
npease a8dc4d8460 Move to .env.local files instead of files on dir 2024-02-20 16:26:01 -05:00
npease 6a0d3f3834 Add marker on user location (keep?) 2024-02-20 15:08:59 -05:00
npease 04cdc500b2 Load default map then center on users computed location 2024-02-20 14:59:33 -05:00
Nicholas Pease 8cd7cafdb5 Add Initial UI and Authentication (#11) 2024-02-20 14:34:40 -05:00
npease ec2fc15a3f Add onboarding, dashboard with relevant API's 2024-02-20 01:08:12 -05:00
Nicholas Pease d7a2382cb5 Restore Dependency Installation 2024-02-19 16:37:16 -05:00
npease 69d5bfe9a9 Remove build stage to remove conflict with local files 2024-02-19 21:34:08 +00:00
npease c555a59cf8 Spaces Matter 2024-02-19 21:26:32 +00:00
npease d20aecdbe8 Use GH Secrets with Workflow 2024-02-19 21:24:45 +00:00
npease 57a8415e52 Update Build Test 2024-02-19 21:02:14 +00:00
npease f19b09c5fd Update Import Paths 2024-02-19 08:17:17 +00:00
npease c6056c385b Favicon Fixes / Cleanup 2024-02-19 08:06:16 +00:00
npease 7420cc63fb Cleanup/condense package.json files 2024-02-19 07:59:18 +00:00
npease c528c6bacf Refactor / Commentate 2024-02-19 07:50:37 +00:00
npease daedd0b068 Initial UI, Login/Register Flow Prelim, Icons 2024-02-19 06:40:11 +00:00
38 changed files with 3339 additions and 70 deletions
+1 -4
View File
@@ -18,12 +18,9 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: 21.6.2
- name: Install dependencies
run: npm install frontend-next/
- name: Lint
run: npm --prefix frontend-next/ run lint
- name: Build Frontend
run: npm --prefix frontend-next/ run build
+4
View File
@@ -1,3 +1,7 @@
# Firebase Stuff
firebase-admin-key.json
firebase*.json
# Logs
logs
*.log
+2 -2
View File
@@ -1,4 +1,4 @@
# ChatMaps
![](/frontend-next/public/logos/logo_transparent.png)
Main repo for ChatMaps, our COS420 Project.
ChatMaps is a web-based social networking service that allows users to connect to others in their local geographic area. It will implement an interactable mapping utility to show general user locations relative to other users, as well as a chat room feature that allows users to start public conversations based on a specified topic. ChatMaps is primarily intended for use in densely populated areas, such as college campuses or metropolitan areas, so people of similar interests can start conversations. The goal of this project is to create a web app that plots locations, gives a radius of the local area, and connects users into different topic-based chat rooms.
@@ -9,7 +9,7 @@ This app shares some similarities to other social networks that implement locati
The live version of this app can be found at:
http://chatmaps.nicholaspease.com
https://chatma.ps/
A local version can be run with:
+2375 -24
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -9,9 +9,13 @@
"lint": "next lint"
},
"dependencies": {
"firebase": "^10.6.0",
"firebase-admin": "^12.0.0",
"next": "^14.1.0",
"pigeon-maps": "^0.21.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-hook-form": "^7.50.1"
},
"devDependencies": {
"autoprefixer": "^10.0.1",
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

Before

Width:  |  Height:  |  Size: 629 B

@@ -0,0 +1,22 @@
import { initializeApp, getApps, cert } from "firebase-admin/app";
import admin from "firebase-admin";
export function customInitApp() {
if (getApps().length <= 0) {
initializeApp({
credential: admin.credential.cert({
type: process.env.FIREBASE_ADMIN_TYPE,
projectId: process.env.FIREBASE_ADMIN_PROJECT_ID,
privateKeyId: process.env.FIREBASE_ADMIN_PRIV_KEY_ID,
privateKey: process.env.FIREBASE_ADMIN_PRIV_KEY?.replace(/\\n/g, "\n"),
clientEmail: process.env.FIREBASE_ADMIN_CLIENT_EMAIL,
clientId: process.env.FIREBASE_ADMIN_CLIENT_ID,
authUri: process.env.FIREBASE_ADMIN_AUTH_URI,
tokenUri: process.env.FIREBASE_ADMIN_TOKEN_URL,
authProviderX509CertUrl: process.env.FIREBASE_ADMIN_AUTH_PROVIDER_X509_CERT_URL,
clientC509CertUrl: process.env.FIREBASE_ADMIN_CLIENT_X509_CERT_URL,
universe_domain: process.env.FIREBASE_ADMIN_UNIVERSE_DOMAIN,
}),
});
}
}
@@ -0,0 +1,17 @@
import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
var config = {
apiKey: "AIzaSyDbDPjQGt-lIjNPeTG-Q5AECM1m0vtOr2c",
authDomain: "chatmaps-3e7fa.firebaseapp.com",
projectId: "chatmaps-3e7fa",
storageBucket: "chatmaps-3e7fa.appspot.com",
messagingSenderId: "771010649524",
appId: "1:771010649524:web:b6e66d3457820c817b26e1",
databaseURL: "https://chatmaps-3e7fa-default-rtdb.firebaseio.com/",
}
var app = getApps().length > 0 ? getApp() : initializeApp(config);
var auth = getAuth(app);
export { auth, app };
+95
View File
@@ -0,0 +1,95 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
// Firebase Imports
import { auth } from "firebase-admin";
import { signInWithEmailAndPassword } from "firebase/auth";
// Lib Imports
import { app, auth as authConfig } from "../firebase-config";
import { customInitApp } from "../firebase-admin";
import { getDatabase, ref, get as firebaseGet } from "firebase/database";
// Needs to "init" on each call to the API
customInitApp();
// Login with Email/Password
async function handleEmailAndPassword(email, password) {
try {
var userCredential = await signInWithEmailAndPassword(authConfig,email,password);
if (userCredential.user.accessToken) {
var token = await auth().verifyIdToken(userCredential.user.accessToken);
var expiresIn = 20 * 60 * 1000; // 20 minutes
var sessionCookie = await auth().createSessionCookie(userCredential.user.accessToken, {expiresIn,});
if (token) {
var database = getDatabase(app)
var user = await firebaseGet(ref(database, `users/${userCredential.user.uid}`));
if (!user.exists()) {
var userOptions = {
name: "user",
value: JSON.stringify({defined: false, uid: userCredential.user.uid}),
maxAge: expiresIn, // 20 mins
httpOnly: true,
secure: true,
};
} else {
var userData = user.val()
userData.uid = userCredential.user.uid
userData.defined = true
var userOptions = {
name: "user",
value: JSON.stringify(userData),
maxAge: expiresIn, // 20 mins
httpOnly: true,
secure: true,
};
}
cookies().set(userOptions);
var options = {
name: "session",
value: sessionCookie,
maxAge: expiresIn, // 20 mins
httpOnly: true,
secure: true,
};
cookies().set(options);
cookies().set({
name: "uid",
value: userCredential.user.uid,
maxAge: expiresIn, // 20 mins
httpOnly: true,
secure: true,
});
return NextResponse.json({ options }, { status: 200 });
}
}
} catch (error) {
return NextResponse.json({ error: error.code }, { status: 401 });
}
}
// Handles POST requests (login requests)
export async function POST(req, res) {
try {
var { email, password } = await req?.json()
return await handleEmailAndPassword(email, password); // need session token
} catch (error) {
return NextResponse.json({ error: "Internal Server Error" },{ status: 500 });
}
}
// Handles GET requests (is session still valid requests)
export async function GET(req) {
var session = cookies().get("session")?.value || "";
//Validate if the cookie exist in the request
if (!session) {
return NextResponse.json({ isLogged: false }, { status: 401 });
} else {
// Validate session cookie
try {
var validation = await auth().verifySessionCookie(session, true);
return NextResponse.json({ isLogged: true, uid: validation.uid, email: validation.email }, { status: 200 });
} catch (error) {
return NextResponse.json({ isLogged: false}, { status: 401 });
}
}
}
@@ -0,0 +1,48 @@
import { NextResponse } from "next/server";
// Lib Imports
import { app } from "../firebase-config";
import { getDatabase, ref, set as firebaseSet } from "firebase/database";
import { cookies } from "next/headers";
async function onboard(onboardingJSON, req) {
var session = req.cookies.get("session");
//Call the authentication endpoint
var res = await fetch(new URL("/api/login", req.url), {headers: {Cookie: `session=${session?.value}`}})
// Login if unauthorized
if (res.status !== 200) {
return NextResponse.json({}, { status: 401 });
}
try {
var expiresIn = 20 * 60 * 1000; // 20 minutes
var { uid, email } = await res.json()
onboardingJSON.email = email
onboardingJSON.uid = uid
onboardingJSON.defined = true
var database = getDatabase(app)
await firebaseSet(ref(database, `users/${uid}`), onboardingJSON);
var userOptions = {
name: "user",
value: JSON.stringify(onboardingJSON),
maxAge: expiresIn, // 20 mins
httpOnly: true,
secure: true,
};
cookies().set(userOptions);
return NextResponse.json({}, { status: 200 });
} catch(error) {
return NextResponse.json({ error: "Internal Server Error: "+error },{ status: 500 });
}
}
// Handles POST requests (login requests)
export async function POST(req, res) {
try {
var onboardingJSON = await req?.json()
return await onboard(onboardingJSON, req);
} catch (error) {
return NextResponse.json({ error: "Internal Server Error" },{ status: 500 });
}
}
@@ -0,0 +1,42 @@
// Import necessary functions
import { createUserWithEmailAndPassword } from "firebase/auth";
import { auth } from "../firebase-config";
import { NextResponse } from "next/server";
// Function to register a new user using Firebase Authentication
export async function registerUser(email, password) {
try {
var userCredential = await createUserWithEmailAndPassword(auth,email,password);
// You can perform additional actions after successful registration, if needed.
return { success: true, userCredential };
} catch (error) {
return { success: false, error: error.message };
}
}
// POST request handler
export async function POST(req, res) {
try {
// Extract email and password from the request body
var { email, password } = await req?.json();
// Check if email and password are provided
if (!email || !password) {
return NextResponse.json(
{ error: "Email and password are required." },
{ status: 400 }
);
}
// Register the user
try {
var userCredential = await createUserWithEmailAndPassword(auth,email,password);
return NextResponse.json({message: "Registration successful.",user: userCredential.user,});
} catch {
return NextResponse.json({ error: registrationResult.error },{ status: 500 });
}
} catch (error) {
// Handle unexpected errors
return NextResponse.json({ error: "Internal Server Error" },{ status: 500 });
}
}
@@ -0,0 +1,10 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function GET(req) {
cookies().delete('user')
cookies().delete('session')
cookies().delete('uid')
return NextResponse.redirect(new URL("/",req.url))
}
+7
View File
@@ -0,0 +1,7 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
export async function GET(req) {
var userData = cookies().get("user")?.value || false
return userData != false? NextResponse.json(JSON.parse(userData)): NextResponse.json({},{status: 203})
}
+19
View File
@@ -0,0 +1,19 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Home",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
);
}
+287
View File
@@ -0,0 +1,287 @@
"use client"
import { useState, useEffect, createContext, useContext } from 'react'
import {Map, Marker, ZoomControl} from "pigeon-maps"
import { Form, useForm } from "react-hook-form";
import { app } from "../api/firebase-config";
import { getDatabase, ref, onValue, get, set} from "firebase/database";
var database = getDatabase(app)
// Data types
function Chat({chatObj}) {
let dateOptions = {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
};
return (
<div className='width-[100%] bg-white rounded-lg mt-1 text-left p-1 grid grid-cols-2 mr-2'>
<div>
{chatObj.user}: {chatObj.body}
</div>
<div className='text-right text-[#d1d1d1]'>
{new Date(chatObj.timestamp).toLocaleString(dateOptions)}
</div>
</div>
)
}
function ChatRoomSidebar({roomObj, click}) {
return (
<div onClick={click} className='border-[black] border-1 shadow-lg p-2 m-2 rounded-lg cursor-pointer'>
<div className='col-span-2'>
<div className='font-bold'>{roomObj.name}</div>
<div className='italic'>{roomObj.description}</div>
</div>
</div>
)
}
function WelcomeMessage() {
//TODO: REALLY GROSS WAY TO GET COOKIES, NEED NEW WAY TO STORE USER DATA WITHOUT API CALLS. THIS PAGE HAS TO BE CLIENT SIDE DUE TO MAPS / GEOLOCATION
const [data, setData] = useState(null)
const [isLoading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/user')
.then((res) => res.json())
.then((data) => {
setData(data)
setLoading(false)
})
}, [])
if (isLoading) return <div></div>
if (!data) return <div></div>
return (
<div className="bg-white rounded-lg m-2 mt-4 text-left p-2 pl-5">
<div>
Welcome, {data.firstName} {data.lastName} ({data.username})
</div>
<div>
Lets see what&apos;s happening in your area.
</div>
</div>
)
}
function Geo({loc}) {
if (loc) {
return (
<Map className="rounded-lg" center={[loc.latitude, loc.longitude]} defaultZoom={14}>
<Marker width={50} anchor={[loc.latitude, loc.longitude]} color="red"/>
<ZoomControl />
</Map>
)
} else {
return (
<Map className="rounded-lg" defaultCenter={[0, 0]} defaultZoom={14}/>
)
}
}
// Main Tabs
function MainTabHome({loc}) {
return (
<>
<WelcomeMessage />
<div className='h-[calc(100%-110px)] m-5 rounded-lg'>
<Geo loc={loc}/>
</div>
</>
)
}
function MainTabChatRoom({room}) {
var { register, control, reset, handleSubmit} = useForm()
const [chats, setData] = useState(null)
const [isLoading, setLoading] = useState(true)
var user
fetch('/api/user')
.then((res) => res.json())
.then((data) => {
user = data
})
var unsubscribeUpdater
useEffect(() => {
unsubscribeUpdater = onValue(ref(database, `/rooms/${room}/chats`), (snapshot) => {
var chatsArr = []
var messages = snapshot.val()
for (var message in messages) {
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,
timestamp: new Date().getTime()
}
set(ref(database,`/rooms/${room}/chats/${user.username}-${new Date().getTime()}`), 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>
)
}
function CreateRoom({loc}) {
var { register, control, reset, handleSubmit} = useForm()
function createRoom(data) {
reset()
var path = String(loc.latitude.toFixed(2)).replace(".","")+"/"+String(loc.longitude.toFixed(2)).replace(".","")
var timestamp = new Date().getTime()
var payload = {
name: data.name,
description: data.description,
timestamp: timestamp,
latitude: loc.latitude,
longitude: loc.longitude,
path: path
}
set(ref(database,`/rooms/${path}/${data.name}-${timestamp}`), payload)
}
return (
<div className='overflow-y-auto h-[90%]'>
<Form control={control} onSubmit={handleSubmit(createRoom)}>
<input {...register("name")} placeholder='Room Name' className='mt-2'/>
<input {...register("description")} placeholder='Room Description' className='mt-2'/><br/>
<div className='mt-3 mb-2'>
Creating room near ({loc.latitude.toFixed(2)}, {loc.longitude.toFixed(2)})
</div>
<button className="p-2 cursor-pointer bg-[#dee0e0] bg-cyan-500 text-white font-bold rounded-full mr-5">Create</button>
</Form>
</div>
)
}
function Home() {
var [tab, setTab] = useState("nearby")
var [mainTab, setMainTab] = useState("home")
var [chatRoom, setChatRoom] = useState("Dev")
const [myRooms, setRoomData] = useState(null)
const [isRoomLoading, setRoomLoading] = useState(true)
useEffect(() => {
fetch('/api/user').then((res) => res.json())
.then((user) => {
get(ref(database, '/users/'+user.uid+'/rooms')).then((snapshot) => {
var rooms = snapshot.val()
var roomArr = []
for (var room in rooms) {
roomArr.push(<ChatRoomSidebar roomObj={rooms[room]} key={rooms[room]} click={() => {setChatRoom(rooms[room].path+"/"+rooms[room].name+"-"+rooms[room].timestamp);setMainTab("chat")}}/>)
}
setRoomData(roomArr)
setRoomLoading(false)
})
})
}, [])
const [location, setLocation] = useState(null);
const [loadingLoc, setLoadingLoc] = useState(true)
const [nearby, setNearby] = useState(null);
const [loadingNearby, setLoadingNearby] = useState(true);
useEffect(() => {
if('geolocation' in navigator) {
// Retrieve latitude & longitude coordinates from `navigator.geolocation` Web API
navigator.geolocation.getCurrentPosition(({ coords }) => {
setLocation(coords)
setLoadingLoc(false)
var nearbyArr = []
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 data = snapshot.val()
for (var room in data) {
nearbyArr.push(<ChatRoomSidebar roomObj={data[room]} click={() => {setChatRoom(data[room].path+"/"+data[room].name+"-"+data[room].timestamp);setMainTab("chat")}}/>)
}
setLoadingNearby(false)
setNearby(nearbyArr)
} else {
setLoadingNearby(false)
}
})
})
}
}, []);
return (
<div className="grid grid-cols-4 auto-cols-max overflow-hidden">
<div className="col-span-3 h-dvh">
<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" && <a onClick={() => {setMainTab("home")}} className="p-2 cursor-pointer bg-[#dee0e0] bg-cyan-500 text-white font-bold rounded-full mr-5">Close Chat</a>}
<a href="/api/signout" className="p-2 cursor-pointer bg-[#dee0e0] bg-cyan-500 text-white font-bold rounded-full">Sign Out</a>
</div>
</div>
<div className="mr-2 h-[calc(100%-110px)]">
{(mainTab == "home" && !loadingLoc) && <MainTabHome loc={location}/>}
{(mainTab == "home" && loadingLoc) && <MainTabHome loc={null}/>}
{mainTab == "chat" && <MainTabChatRoom room={chatRoom}/>}
</div>
</div>
<div className="h-dvh">
<div className="bg-white shadow-2xl rounded-lg m-2 h-[98%]">
<div className='p-2'>
<div className='p-1 rounded-lg grid grid-cols-3 bg-white'>
<div className={tab == "nearby"? 'select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0] bg-[#D3D3D3]': 'select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0]'} onClick={() => {setTab("nearby")}}>Nearby</div>
<div className={tab == "rooms"? 'select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0] bg-[#D3D3D3]': 'select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0]'} onClick={() => {setTab("rooms")}}>My Rooms</div>
<div className={tab == "create"? 'select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0] bg-[#D3D3D3]': 'select-none p-1 cursor-pointer rounded-lg hover:bg-[#C0C0C0]'} onClick={() => {setTab("create")}}>Create</div>
</div>
</div>
{tab == "nearby" && <div className='overflow-y-auto h-[90%]'>
<div>
{(!nearby && !loadingNearby) && <div>No Nearby Rooms<br/>Create One?</div>}
{loadingNearby && <div>Loading...</div>}
{nearby}
</div>
</div>}
{tab == "rooms" && <div className='overflow-y-auto h-[90%]'>
<div>
{isRoomLoading && <div>Loading</div>}
{(!myRooms && !isRoomLoading) && <div>No User Saved Rooms</div>}
{myRooms}
</div>
</div>}
{(tab == "create" && !loadingLoc) && <CreateRoom loc={location}/>}
{(tab == "create" && loadingLoc) && <div>Loading...</div>}
</div>
</div>
</div>
)
}
export default Home;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+12 -2
View File
@@ -2,17 +2,20 @@
@tailwind components;
@tailwind utilities;
main {
body {
background-color: aliceblue;
text-align: center;
text-wrap:pretty;
}
button {
background-color: rgb(205, 205, 205);
background: #dee0e0;
border-color: black;
border: 5px;
border-radius: 5px;
padding: 5px;
margin: 5px;
filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15));
}
button:hover {
@@ -23,5 +26,12 @@ input {
border: 1px solid black;
border-radius: 4px;
padding: 10px 10px;
margin: 5px;
}
input.err {
border: 2px solid red;
border-radius: 4px;
padding: 10px 10px;
margin: 5px;
}
+4 -2
View File
@@ -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>
);
}
+19
View File
@@ -0,0 +1,19 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Login",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
);
}
+53
View File
@@ -0,0 +1,53 @@
"use client";
import { useForm, Form } from "react-hook-form";
import { useRouter } from "next/navigation";
import "../globals.css"
function Login() {
var router = useRouter();
//var { register, handleSubmit } = useForm();
var { register, control, setError, formState: { errors, isSubmitting, isSubmitted } } = useForm()
return (
<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 action="/api/login" encType={'application/json'}
onSuccess={() => {
router.push("/app");
}}
onError={() => {
const formError = { type: "server", message: "Username or Password Incorrect" }
// set same error in both:
setError('password', formError)
setError('email', formError)
}}
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" stroke="currentColor" stroke-width="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>
)
}
export default Login;
@@ -0,0 +1,19 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Onboarding",
description: "ChatMaps: Social Media for College Students"
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
);
}
+45
View File
@@ -0,0 +1,45 @@
"use client";
import "../globals.css"
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
function Onboarding() {
var router = useRouter();
var { register, handleSubmit } = useForm();
async function Onboard(data) {
const res = await fetch("/api/onboard", {
method: "POST",
body: JSON.stringify(data ? data : {}),
});
if (res.ok) {
router.push("/app");
} else {
router.push("/login");
}
}
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="bg-[#dee0e0] m-5">Save</button>
</form>
</div>
</div>
</div>
)
}
export default Onboarding;
+37 -24
View File
@@ -1,27 +1,40 @@
export default function Home() {
return (
<main className="flex min-h-screen flex-col justify-between p-24">
<h1>Welcome to ChatMaps.</h1>
"use client"
import { useState, useEffect } from 'react'
<div id="room">
<label>Room </label>
<select>
<option>Room 1</option>
<option>Room 2</option>
<option>Room 3</option>
</select>
<button>Join Room</button>
</div>
<div id="message">
<label>Enter a message</label>
<input />
<button>Send</button>
</div>
</main>
);
function Home() {
const [statusCode, setData] = useState(null)
const [isLoading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/user')
.then((res) => res.status)
.then((status) => {
setData(status)
setLoading(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">
{(statusCode == 203 || isLoading) &&
<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>
}
{statusCode == 200 && <a href="/app"><button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">Continue to App</button></a>}
</div>
</div>
</div>
</div>
)
}
export default Home;
+19
View File
@@ -0,0 +1,19 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Register",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
);
}
+62
View File
@@ -0,0 +1,62 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm, Form } from "react-hook-form";
import "../globals.css"
import { useState } from "react";
function Register() {
var { register, control, setError, handleSubmit, formState: { errors } } = useForm()
var router = useRouter();
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;
};
const onSubmit = (data) => {
if (passwordMatch(data)) {
setPasswordMismatch(false);
router.push("/success");
} else{
setPasswordMismatch(true);
return;
}
}
return (
<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={handleSubmit(onSubmit)}
onSuccess={() => {
router.push("/app");
}}
action="/api/register"
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>
)
}
export default Register;
@@ -0,0 +1,26 @@
import { Inter } from "next/font/google";
import "../../globals.css";
import { Header, Sidebar } from "./shared"
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
<div className="grid grid-cols-4 auto-cols-max">
<div className="col-span-3 h-dvh">
<Header/>
{children}
</div>
<Sidebar/>
</div>
</body>
</html>
);
}
+28
View File
@@ -0,0 +1,28 @@
function MessagesDisplayBox() {
return (
<div className="h-[98%]">
Messages Stream in Here
</div>
)
}
function MessageSendBox() {
return (
<div className="bg-white rounded-lg shadow-2xl w-[98%] m-2">
Message Sender
</div>
)
}
function Home() {
return (
<div className="">
<MessagesDisplayBox/>
<MessageSendBox/>
</div>
)
}
export default Home;
@@ -0,0 +1,18 @@
export function Header() {
return (
<div className="m-2 rounded-lg h-[60px] bg-white shadow-2xl">
<img src="/logos/logo_transparent_inverse.png" className="h-[60px]"></img>
</div>
)
}
export function Sidebar() {
return (
<div className="h-dvh">
<div className="bg-white shadow-2xl rounded-lg m-2 h-[98%]">
Sidebar
</div>
</div>
)
}
+57
View File
@@ -0,0 +1,57 @@
// src/middleware.js
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
export async function middleware(req, res) {
const session = await req.cookies.get("session");
if (req.nextUrl.pathname !== "/login" && req.nextUrl.pathname != "/register") {
// Login if not logged in
if (!session) {
return NextResponse.redirect(new URL("/login", req.url));
}
//Call the authentication endpoint
const responseAPI = await fetch(new URL("/api/login", req.url), {
headers: {
Cookie: `session=${session?.value}`,
},
});
// Login if unauthorized
if (responseAPI.status !== 200) {
return NextResponse.redirect(new URL("/login", req.url));
}
// If new user, redirect to onboarding
var user = JSON.parse(req.cookies.get("user").value)
if (user.defined) {
return NextResponse.next();
} else {
return NextResponse.redirect(new URL("/onboarding", req.url));
}
} else {
// Currently in the /login or /register, if user is authenticated, go ahead and direct them to the app
if (session) {
const responseAPI = await fetch(new URL("/api/login", req.url), {
headers: {
Cookie: `session=${session?.value}`,
},
});
if (responseAPI.status == 200) {
return NextResponse.redirect(new URL("/app", req.url))
} else {
return NextResponse.next() // Unauthenticated, continue
}
} else {
return NextResponse.next() // Not logged in, direct to login
}
}
}
//Protected routes
export const config = {
matcher: ['/((?!onboarding|api|_next/static|_next/image|auth|favicon.ico|robots.txt|images|logo|$).*)',],
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
};
-9
View File
@@ -5,14 +5,5 @@ module.exports = {
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
};
+6
View File
@@ -0,0 +1,6 @@
{
"name": "ChatMaps",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}