diff --git a/package-lock.json b/package-lock.json index 70d1847e..69afe483 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,9 @@ "eslint-config-next": "13.0.7", "firebase": "^9.23.0", "firebase-admin": "^11.11.1", + "framer-motion": "^12.4.2", "js-cookie": "^3.0.1", + "lucide-react": "^0.474.0", "next": "13.0.7", "nextjs-progressbar": "^0.0.16", "react": "18.2.0", @@ -3303,6 +3305,32 @@ "url": "https://www.patreon.com/infusion" } }, + "node_modules/framer-motion": { + "version": "12.4.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.2.tgz", + "integrity": "sha512-pW307cQKjDqEuO1flEoIFf6TkuJRfKr+c7qsHAJhDo4368N/5U8/7WU8J+xhd9+gjmOgJfgp+46evxRRFM39dA==", + "dependencies": { + "motion-dom": "^12.0.0", + "motion-utils": "^12.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4673,6 +4701,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" }, + "node_modules/lucide-react": { + "version": "0.474.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.474.0.tgz", + "integrity": "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/markdown-it": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", @@ -4799,6 +4835,19 @@ "node": ">=10" } }, + "node_modules/motion-dom": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz", + "integrity": "sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==", + "dependencies": { + "motion-utils": "^12.0.0" + } + }, + "node_modules/motion-utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz", + "integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -9021,6 +9070,16 @@ "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", "dev": true }, + "framer-motion": { + "version": "12.4.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.2.tgz", + "integrity": "sha512-pW307cQKjDqEuO1flEoIFf6TkuJRfKr+c7qsHAJhDo4368N/5U8/7WU8J+xhd9+gjmOgJfgp+46evxRRFM39dA==", + "requires": { + "motion-dom": "^12.0.0", + "motion-utils": "^12.0.0", + "tslib": "^2.4.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -10036,6 +10095,12 @@ } } }, + "lucide-react": { + "version": "0.474.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.474.0.tgz", + "integrity": "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==", + "requires": {} + }, "markdown-it": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", @@ -10120,6 +10185,19 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "optional": true }, + "motion-dom": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz", + "integrity": "sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==", + "requires": { + "motion-utils": "^12.0.0" + } + }, + "motion-utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz", + "integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index ead2f7b6..4e4cc75f 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "eslint-config-next": "13.0.7", "firebase": "^9.23.0", "firebase-admin": "^11.11.1", + "framer-motion": "^12.4.2", "js-cookie": "^3.0.1", "lucide-react": "^0.474.0", "next": "13.0.7", diff --git a/src/components/Common/Navbar.tsx b/src/components/Common/Navbar.tsx index 38c923a0..72759046 100644 --- a/src/components/Common/Navbar.tsx +++ b/src/components/Common/Navbar.tsx @@ -6,6 +6,7 @@ import { FiLogOut } from 'react-icons/fi' import { useRouter } from 'next/router' import { useAuth } from '../../contexts/auth' import Image from 'next/image' +import Link from 'next/link' export const Navbar: FC<{}> = ({}) => { const router = useRouter() @@ -23,7 +24,7 @@ export const Navbar: FC<{}> = ({}) => { }, [router, activeRef, activeRef.current?.offsetWidth]) return ( -
+
{user ? ( @@ -33,7 +34,7 @@ export const Navbar: FC<{}> = ({}) => { > user = ({}) => {

Logout

+ +

Profile

+ + +
)}
@@ -103,9 +109,7 @@ export const Navbar: FC<{}> = ({}) => { > Courses - - - { router.push('/rateprofessor') @@ -114,7 +118,13 @@ export const Navbar: FC<{}> = ({}) => { > Rate Professor - + router.push('/template')} + className={`cursor-pointer p-3 relative before:content-[''] before:absolute before:bottom-[-7px] before:left-0 before:w-full before:h-[5px] before:bg-primary/40 before:rounded-[8px_8px_0_0] before:opacity-0 before:duration-100 text-gray-600 hover:before:opacity-100 hover:before:bottom-0 hover:text-gray-800`} + > + Templates + {}, @@ -32,11 +32,12 @@ export const AuthProvider = ({ children }: any) => { const [photoURL, setPhotoURL] = useState(''); const [displayName, setDisplayName] = useState(''); const [loading, setLoading] = useState(true); + const auth = firebaseAuth; + - // Functions const logout = async () => { try { - await signOut(firebaseAuth); + await signOut(auth); cookies.remove('accessToken'); setUser(null); setPhotoURL(''); @@ -49,9 +50,8 @@ export const AuthProvider = ({ children }: any) => { const login = async () => { const provider = new GoogleAuthProvider(); - try { - const res = await signInWithPopup(firebaseAuth, provider); + const res = await signInWithPopup(auth, provider); const { user }: any = res; const { accessToken } = user; @@ -67,7 +67,7 @@ export const AuthProvider = ({ children }: any) => { setUser(userData); setPhotoURL(picture); setDisplayName(name); - return isAdmin + return isAdmin; } toast.success('Login Successful'); } @@ -75,7 +75,6 @@ export const AuthProvider = ({ children }: any) => { toast.error((err as FirebaseError).message); } }; - async function loadUserFromCookie() { const accessToken = cookies.get('accessToken'); if (accessToken) { @@ -114,9 +113,9 @@ export const AuthProvider = ({ children }: any) => { setLoading(false); } - // Effects + useEffect(() => { - const unsubscribe = onAuthStateChanged(firebaseAuth, async (authUser: FirebaseUser | null) => { // ✅ Use firebaseAuth + const unsubscribe = onAuthStateChanged(firebaseAuth, async (authUser: FirebaseUser | null) => { if (authUser) { const token = await authUser.getIdToken(); @@ -158,14 +157,13 @@ export const AuthProvider = ({ children }: any) => { export const useAuth = () => { const authContext = useContext(AuthContext); - if (!authContext) { - throw new Error("useAuth must be used within an AuthProvider"); + throw new Error('useAuth must be used within an AuthProvider'); } return authContext as { isAuthenticated: boolean; - user: User | null; // Use the defined User type + user: User | null; photoURL: string; displayName: string; login: () => void; @@ -174,14 +172,13 @@ export const useAuth = () => { }; }; - - export const ProtectRoute = ({ children }: any) => { - const router = useRouter(); - const { isAuthenticated, loading }: any = useAuth(); + const router = useRouter(); + const { isAuthenticated, loading } = useAuth(); if (loading || (!isAuthenticated && router.pathname !== '/login')) { return

Loading...

; } return children; }; + diff --git a/src/pages/api/db/profile/index.ts b/src/pages/api/db/profile/index.ts new file mode 100644 index 00000000..b5153383 --- /dev/null +++ b/src/pages/api/db/profile/index.ts @@ -0,0 +1,62 @@ +import { firestore } from '../../../../utils/firebaseInit'; +import { + collection, + doc, + getDoc, + getDocs, + updateDoc, + setDoc, +} from 'firebase/firestore'; +import { NextApiRequest, NextApiResponse } from 'next'; + +const profilesCollection = collection(firestore, 'profiles'); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method, body, query } = req; + + switch (method) { + case 'GET': + try { + const { id } = query; + if (!id || typeof id !== 'string') { + return res.status(400).json({ message: 'Profile ID is required' }); + } + + const profileDoc = await getDoc(doc(profilesCollection, id)); + if (!profileDoc.exists()) { + return res.status(404).json({ message: 'Profile not found' }); + } + + res.status(200).json({ message: 'Profile Fetched', result: profileDoc.data() }); + } catch (err) { + res.status(500).json({ message: 'Error fetching profile', error: err }); + } + break; + + case 'PUT': + try { + const { id } = query; + if (!id || typeof id !== 'string') { + return res.status(400).json({ message: 'Profile ID is required' }); + } + + const profileRef = doc(profilesCollection, id); + const profileDoc = await getDoc(profileRef); + + if (!profileDoc.exists()) { + await setDoc(profileRef, body); + } else { + await updateDoc(profileRef, body); + } + + res.status(200).json({ message: 'Hoorayy! Profile Updated' }); + } catch (err) { + res.status(500).json({ message: 'Error updating profile', error: err }); + } + break; + + default: + res.status(405).json({ message: 'Method Not Allowed' }); + } +} + diff --git a/src/pages/discussion.tsx b/src/pages/discussion.tsx new file mode 100644 index 00000000..6c6384fb --- /dev/null +++ b/src/pages/discussion.tsx @@ -0,0 +1,96 @@ +import { useState, useEffect } from 'react'; +import { firestore } from '../utils/firebaseInit'; +import { collection, addDoc, query, orderBy, onSnapshot, serverTimestamp } from 'firebase/firestore'; +import { useAuth } from '../contexts/auth'; +import { FaThumbsUp, FaReply, FaThumbtack, FaEdit, FaTrash } from 'react-icons/fa'; +import bgImage from 'assets/discussion-bg.png'; + + +const DiscussionPage = () => { + const { user } = useAuth(); + const [messages, setMessages] = useState([]); + const [newMessage, setNewMessage] = useState(''); + const [selectedMessage, setSelectedMessage] = useState(null); + + useEffect(() => { + const messagesCollection = collection(firestore, 'discussion'); + const q = query(messagesCollection, orderBy('createdAt', 'asc')); + + const unsubscribe = onSnapshot(q, (querySnapshot) => { + const fetchedMessages = querySnapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })); + setMessages(fetchedMessages); + }); + + return () => unsubscribe(); + }, []); + + const sendMessage = async () => { + if (!newMessage.trim()) return; + try { + await addDoc(collection(firestore, 'discussion'), { + text: newMessage, + author: user?.displayName || 'Anonymous', + createdAt: serverTimestamp(), + }); + setNewMessage(''); + } catch (error) { + console.error('Error sending message:', error); + } + }; + + return ( +
+

💬 Welcome to the Discussion

+ +
+
+
+ {messages.length === 0 && ( +

No messages yet. Start the conversation!

+ )} + {messages.map((msg) => ( +
setSelectedMessage(msg.id === selectedMessage ? null : msg.id)} + > +

{msg.author}

+

{msg.text}

+ {selectedMessage === msg.id && ( +
+ + + + + +
+ )} +
+ ))} +
+ +
+ setNewMessage(e.target.value)} + className="flex-1 p-4 border rounded-lg shadow-md focus:outline-none focus:ring-2 focus:ring-blue-400 text-lg" + placeholder="Type a message..." + /> + +
+
+
+ ); +}; + +export default DiscussionPage; + diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx new file mode 100644 index 00000000..2d6f85e9 --- /dev/null +++ b/src/pages/profile.tsx @@ -0,0 +1,198 @@ +import React, { useEffect, useState } from 'react'; +import { ProfileData } from '../types/profileColumnData'; +import { toast } from 'react-hot-toast'; +import { updateProfile } from '../services/db/profile/updateProfile'; +import { useAuth } from '../contexts/auth'; +import { firestore } from '../utils/firebaseInit'; +import { collection, query, where, getDocs } from 'firebase/firestore'; +import { useRouter } from 'next/router'; + +const ProfilePage: React.FC = () => { + const router = useRouter(); + const [profile, setProfile] = useState(null); + const [editMode, setEditMode] = useState(false); + const [formData, setFormData] = useState({ + name: '', + email: '', + branch: '', + college: '', + bio: '', + profileImage: '', + }); + + useEffect(() => { + const fetchProfile = async () => { + try { + const storedProfile = localStorage.getItem("userProfile"); + + if (storedProfile) { + const parsedProfile = JSON.parse(storedProfile); + setProfile(parsedProfile); + setFormData(parsedProfile); + } else { + const userEmail = "john.doe@example.com"; + const profilesCollection = collection(firestore, "profiles"); + const q = query(profilesCollection, where("email", "==", userEmail)); + const querySnapshot = await getDocs(q); + + if (!querySnapshot.empty) { + const userProfile = querySnapshot.docs[0].data() as ProfileData; + setProfile(userProfile); + setFormData(userProfile); + + localStorage.setItem("userProfile", JSON.stringify(userProfile)); + } else { + console.warn("Profile not found for email:", userEmail); + } + } + } catch (error) { + console.error("Error fetching profile:", error); + } + }; + + fetchProfile(); + }, []); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSave = async () => { + try { + await updateProfile(formData); + setProfile(formData); + setEditMode(false); + localStorage.setItem("userProfile", JSON.stringify(formData)); + } catch (err) { + toast.error('Failed to update profile. Please try again.'); + } + }; + + const handleCancel = () => { + setEditMode(false); + setFormData(profile!); + }; + + return ( +
+
+ {profile && ( +
+
+ Profile + +
+ + {editMode ? ( +
+ + + + +