diff --git a/README.md b/README.md index 90052cf..8e7ff18 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,21 @@ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ``` - +# Routing: +- run: +` +npm run dev +` -## Overview :sparkles: -- +# Current utility: +- Login / SignUp with google +- Dashboard / Landing Page on login. -## Development :computer: -![GitHub](https://img.shields.io/github/license/LaurierComputingSociety/Pod2) -![GitHub pull requests](https://img.shields.io/github/issues-pr/LaurierComputingSociety/Pod2) - -### Requirements -- - -### Setup -```sh -$ -``` +# Next: +- Actual email & password login +- Username Setup +- User Settings Setup +- Other routes only available once logged in. +# Service used: +- FireBase diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..2a2e4b3 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./*"] + } + } +} diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..767719f --- /dev/null +++ b/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/package.json b/package.json new file mode 100644 index 0000000..10ca317 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "pod2", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@firebase/app": "^0.9.24", + "@firebase/auth": "^1.5.0", + "@firebase/firestore": "^4.4.0", + "@firebase/storage": "^0.12.0", + "firebase": "^10.7.0", + "next": "14.0.3", + "react": "^18", + "react-dom": "^18", + "react-router-dom": "^6.20.0" + }, + "devDependencies": { + "@types/next": "^9.0.0", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10.0.1", + "eslint": "^8", + "eslint-config-next": "14.0.3", + "postcss": "^8", + "tailwindcss": "^3.3.0", + "typescript": "^5" + } +} diff --git a/pages/ChatPage.tsx b/pages/ChatPage.tsx new file mode 100644 index 0000000..7a4ac22 --- /dev/null +++ b/pages/ChatPage.tsx @@ -0,0 +1,53 @@ +import React, { useEffect, useState } from 'react'; +import { onAuthStateChanged, User } from 'firebase/auth'; +import { auth, db } from '../src/app/firebase/firebase'; +import { doc, getDoc } from 'firebase/firestore'; +import { useRouter } from 'next/router'; +import NavBar from '../src/app/Utility/NavBar' +import './main.css' + +const ChatRenderingPage = () => { + const [currentUser, setCurrentUser] = useState(null); + const [username, setUsername] = useState(null); + const [isReady, setIsReady] = useState(false); // New state to control rendering + const router = useRouter(); + + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, async (user) => { + if (user) { + setCurrentUser(user); + const userDoc = await getDoc(doc(db, 'users', user.uid)); + if (userDoc.exists()) { + setUsername(userDoc.data().username); + } + setIsReady(true); // Set ready state to true if user is authenticated + } else { + router.push('/login'); + } + }); + + return () => unsubscribe(); + }, [router]); + + useEffect(() => { + const timeoutId = setTimeout(() => { + setIsReady(true); + }, 3000); + + return () => clearTimeout(timeoutId); // clears timeout + }, []); + + if (!isReady) { + return
Loading...
; // Loading instead of rendering chat temporarely. + } + + return ( + <> + +
ChatRenderingPage
+ + + ); +} + +export default ChatRenderingPage; diff --git a/pages/landing.tsx b/pages/landing.tsx new file mode 100644 index 0000000..ad5f4f4 --- /dev/null +++ b/pages/landing.tsx @@ -0,0 +1,80 @@ +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; +import { auth, db } from '../src/app/firebase/firebase'; +import { onAuthStateChanged, signOut, User } from 'firebase/auth'; +import { doc, getDoc } from 'firebase/firestore'; +import './main.css'; +import AvatarRendering from '../src/app/logic/AvatarUpload' +import NavBar from '../src/app/Utility/NavBar' + + +const Landing = () => { + const [currentUser, setCurrentUser] = useState(null); + const [username, setUsername] = useState(null); // State for username + const [avatarUrl, setAvatarUrl] = useState(null); + const router = useRouter(); + + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, async (user) => { + if (user) { + setCurrentUser(user); + // gets the username and avatar URL from Firestore + const userDoc = await getDoc(doc(db, 'users', user.uid)); + if (userDoc.exists()) { + setUsername(userDoc.data().username); + //the avatar URL is stored under the key 'avatarURL' + const avatarUrl = userDoc.data().avatarURL; + setAvatarUrl(avatarUrl); + } + } else { + router.push('/login'); + } + }); + + return () => unsubscribe(); + }, [router]); + + + const handleLogout = async () => { + try { + await signOut(auth); + setCurrentUser(null); + setUsername(null); + router.push('/login'); + } catch (error) { + console.error('Error logging out:', error); + } + }; + + return ( + <> + +
+ {avatarUrl && ( + User Avatar +)} +
+

Welcome to the Landing Page

+

Email: {currentUser?.email}

+ {username &&

Logged in as {username}

} + +
+
+ {currentUser && } +
+
+ + ); + +}; + +export default Landing; diff --git a/pages/login.tsx b/pages/login.tsx new file mode 100644 index 0000000..6ec827b --- /dev/null +++ b/pages/login.tsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { useRouter } from 'next/router'; +import { auth } from '../src/app/firebase/firebase'; +import { signInWithEmailAndPassword } from 'firebase/auth'; +import './main.css' + +const Login = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const router = useRouter(); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + try { + await signInWithEmailAndPassword(auth, email, password); + // Redirect to the main route after successful login + router.push('/landing'); + } catch (error) { + if (error instanceof Error) { + console.error('Error logging in:', error.message); + } else { + console.error('Error logging in:', error); + } + } + }; + + return ( + +
+
+

Login

+
+ + setEmail(e.target.value)} + className="mt-1 p-2 w-full border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" + /> +
+
+ + setPassword(e.target.value)} + className="mt-1 p-2 w-full border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" + /> +
+ +

No account? Sign Up

+
+
+ ); +}; + +export default Login; diff --git a/pages/main.css b/pages/main.css new file mode 100644 index 0000000..ea734f5 --- /dev/null +++ b/pages/main.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} diff --git a/pages/signup.tsx b/pages/signup.tsx new file mode 100644 index 0000000..de79e9e --- /dev/null +++ b/pages/signup.tsx @@ -0,0 +1,78 @@ +"use client" +import './main.css'; +import { useRouter } from 'next/router'; +import React, { useState, useEffect } from 'react'; +import { auth, db } from '../src/app/firebase/firebase'; +import { createUserWithEmailAndPassword } from 'firebase/auth'; +import { setDoc, doc } from 'firebase/firestore'; + +const SignUp = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [username, setUsername] = useState(''); // State for username + const [routerReady, setRouterReady] = useState(false); + const router = useRouter(); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const userCredential = await createUserWithEmailAndPassword(auth, email, password); + // Store the username in Firestore under the user's UID + await setDoc(doc(db, "users", userCredential.user.uid), { + username: username + }); + console.log('User created:', userCredential.user); + + // Redirect to /landing on success + router.push('/landing'); + } catch (error) { + if (error instanceof Error) { + console.error('Error signing up:', error.message); + } else { + console.error('Error signing up:', error); + } + } + }; + + + return ( + <> +
+
+

Sign Up

+

Welcome To SpeedChat


+ setUsername(e.target.value)} + /> + setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + +

Already have an account? Login

+
+
+ + ); +}; + +export default SignUp; diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..d2f8422 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/Utility/NavBar.tsx b/src/app/Utility/NavBar.tsx new file mode 100644 index 0000000..9a095ef --- /dev/null +++ b/src/app/Utility/NavBar.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import './nav.css'; + +const NavBar = () => { + return ( + + ); +} + +export default NavBar; diff --git a/src/app/Utility/nav.css b/src/app/Utility/nav.css new file mode 100644 index 0000000..1ab6520 --- /dev/null +++ b/src/app/Utility/nav.css @@ -0,0 +1,52 @@ +.navbar { + background-color: #F1EFEF; /* Dark transparent background */ + padding: 10px 20px; + text-align: center; + backdrop-filter: blur(10px); + } + + .nav-list { + list-style: none; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + } + + .nav-item { + margin: 0 15px; + } + + .nav-link { + color: #272829; + text-decoration: none; + font-size: 18px; + position: relative; + transition: color 0.3s; + } + + .nav-link:hover { + color: #4b9cd3; /* Change color on hover */ + } + + .nav-link::after { + content: ''; + position: absolute; + width: 0; + height: 2px; + display: block; + margin-top: 5px; + right: 0; + background: #4b9cd3; + transition: width 0.3s ease; + -webkit-transition: width 0.3s ease; + -moz-transition: width 0.3s ease; + -o-transition: width 0.3s ease; + } + + .nav-link:hover::after { + width: 100%; + left: 0; + background-color: #4b9cd3; + } + \ No newline at end of file diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/src/app/favicon.ico differ diff --git a/src/app/firebase/firebase.js b/src/app/firebase/firebase.js new file mode 100644 index 0000000..335b765 --- /dev/null +++ b/src/app/firebase/firebase.js @@ -0,0 +1,25 @@ +import { initializeApp } from "firebase/app"; +import { getAuth } from "firebase/auth"; +import { getFirestore } from 'firebase/firestore'; +import { getStorage } from 'firebase/storage'; + +// https://firebase.google.com/docs/web/setup#available-libraries + +// Your web app's Firebase configuration +const firebaseConfig = { + apiKey: "AIzaSyCK7c05pCHjuJIzb9GXzjjJ-3E9NVCyypk", + authDomain: "speedchat-4981a.firebaseapp.com", + projectId: "speedchat-4981a", + storageBucket: "speedchat-4981a.appspot.com", + messagingSenderId: "895036818176", + appId: "1:895036818176:web:f6485c75a908f8d716a9e5" +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); +// Initialize Firebase Authentication and get a reference to the service +const db = getFirestore(app); +const auth = getAuth(app); +const storage = getStorage(app); +export default app; +export { auth, db, storage }; \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..ea734f5 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..13d74d1 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import './globals.css' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'SpeedChat', + description: '', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/src/app/logic/AvatarUpload.tsx b/src/app/logic/AvatarUpload.tsx new file mode 100644 index 0000000..b52bda5 --- /dev/null +++ b/src/app/logic/AvatarUpload.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { storage, db } from '../firebase/firebase'; +import { FirebaseError } from 'firebase/app'; +import { UploadTaskSnapshot, ref, getDownloadURL , uploadBytesResumable} from 'firebase/storage'; // Import Firebase Storage types +import { collection, doc, updateDoc } from 'firebase/firestore'; // Import Firestore types + + +const AvatarUpload = ({ userId }: { userId: string }) => { + const [image, setImage] = useState(null); + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file && (file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif')) { + setImage(file); + } else { + alert('Please select a valid image (JPG, PNG, GIF).'); + } + }; + + const handleUpload = () => { + if (image) { + const imageRef = ref(storage, `avatars/${userId}/${image.name}`); + const uploadTask = uploadBytesResumable(imageRef, image); + + uploadTask.on( + 'state_changed', + (snapshot: UploadTaskSnapshot) => { + // nothing to handle for now + }, + (error: FirebaseError) => { + console.error('Error uploading image:', error); + }, + () => { + // Upload completed, get the download URL and store it in Firestore + getDownloadURL(imageRef).then((url) => { + // Store the URL in Firestore, associate it with the user, etc. + updateDoc(doc(db, 'users', userId), { + avatarURL: url, + }); + alert('Avatar uploaded successfully!'); + }); + } + ); + } + }; + + return ( +
+ + +
+ ); +}; + +export default AvatarUpload; diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..1f5fb77 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,30 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +export default function Home() { + return ( +
+
+

Welcome to SpeedCat

+

Join us to explore exciting features!

+ + +
+ +
+
+
+ ); +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..af2aff6 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,13 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} + diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..1af3b8f --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,20 @@ +import type { Config } from 'tailwindcss' + +const config: Config = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './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: [], +} +export default config diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..14bd9ea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +}