diff --git a/app/SignInWithGithub/SignInWithGithub.tsx b/app/SignInWithGithub/SignInWithGithub.tsx new file mode 100644 index 000000000..871cbc53f --- /dev/null +++ b/app/SignInWithGithub/SignInWithGithub.tsx @@ -0,0 +1,97 @@ +"use client" +import { signInWithPopup,GithubAuthProvider,signOut } from 'firebase/auth' +import React, { useEffect, useState } from 'react' +import {auth,provider} from '../../lib/firebase-config' +import { useRouter } from 'next/router' +import Image from 'next/image' +import { toast,ToastContainer } from 'react-toastify' +import 'react-toastify/dist/ReactToastify.css'; + +const SignInWithGithub=()=>{ + const router = useRouter(); + const [imageURL, setImageURL] = useState(null); + const [authenticated,setAuthenticated] = useState(false); + const signIn = async () => { + try { + const result = await signInWithPopup(auth, provider); + const credential = GithubAuthProvider.credentialFromResult(result); + console.log("Credential",credential); + console.log("Result: ",result); + const token: string = await result.user.getIdToken(); + const username = result.user.displayName; + localStorage.setItem('accessToken', token); + const currDate = new Date().getTime; + document.cookie = `accessToken=${token};path=/; expires=${currDate}`; + const imgURL = result.user.photoURL; + localStorage.setItem('imageURL',imgURL as string); + setImageURL(imgURL); + setAuthenticated(true); + + fetch('/api/auth',{ + method: 'POST', + headers: { + Authorization: `Bearer ${token}` + } + }).then((response)=>{ + if(response.status===200) + { + router.push('/'); + toast.success(`${username} is authenticated successfully`); + } + }) + } + catch (error) { + console.error('Error signing in:', error); + } + } + + const handleSignOut=async()=>{ + try { + await signOut(auth); + router.push("/"); + toast.success("You are successfully logged out!!"); + const currDate = new Date().getTime; + document.cookie = `accessToken=; expires=${currDate}; path=/;`; + console.log(document.cookie); + localStorage.removeItem('accessToken'); + localStorage.removeItem('imageURL'); + setAuthenticated(false); + setImageURL(null); + } catch (error) { + console.log(error); + } + } + + useEffect(()=>{ + const storedToken = localStorage.getItem('accessToken'); + const storedImageURL = localStorage.getItem('imageURL'); + if (storedToken && storedImageURL) { + setImageURL(storedImageURL); + setAuthenticated(true); + } + },[]) + return ( + <> +
+ {authenticated && ( +
+ {imageURL && User Profile} +
+ )} + +
+ {authenticated ? ( + + ) : ( + + )} + + ) + +} + +export default SignInWithGithub; diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts new file mode 100644 index 000000000..18a8abdf0 --- /dev/null +++ b/app/api/auth/route.ts @@ -0,0 +1,68 @@ +import { auth } from "firebase-admin"; +import { cookies } from "next/headers" +import { NextRequest,NextResponse } from "next/server"; +import { customInitApp } from "lib/firebase-admin-config"; + +customInitApp(); + +export async function POST(request: NextRequest){ + const authorization = request.headers.get("Authorization"); + if(authorization && authorization.startsWith('Bearer ')) + { + const idToken = authorization.split("Bearer ")[1]; + + const decodedToken = await auth().verifyIdToken(idToken); + if(decodedToken) + { + const expiresIn = 60 * 60 * 24 * 5 * 1000; + const sessionCookie = await auth().createSessionCookie(idToken,{ + expiresIn + }); + + const options = { + name: "Session", + value: sessionCookie, + maxAge: expiresIn, + httpOnly: true, + secure: true, + }; + cookies().set(options); + } + } + return NextResponse.json({}, { status: 200 }); +} + +export async function GET() +{ + const session = cookies().get("accessToken")?.value || ""; + if(!session) + { + return NextResponse.json({isLogged: false},{status: 401}); + } + const decodedClaims = await auth().verifySessionCookie(session,true); + if(!decodedClaims) + { + return NextResponse.json({isLogged: false},{status: 401}); + } + return NextResponse.json({ isLogged: true }, { status: 200}); +} + +export async function DELETE(request: NextRequest, response: NextResponse) { + const session = request.cookies.get("accessToken"); + + if (!session) { + return NextResponse.json({ isLogged: false }, { status: 401 }); + } + + try { + response.cookies.set('accessToken', '', { + httpOnly: true, + maxAge: 0, + }) + + return NextResponse.json({}, { status: 200 }); + } catch (error) { + console.error('Error in handleLogout:', error); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 000000000..a14e64fcd --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/components/Cards/Card.tsx b/components/Cards/Card.tsx index 86d7cb8e0..31dfc4226 100644 --- a/components/Cards/Card.tsx +++ b/components/Cards/Card.tsx @@ -1,13 +1,20 @@ -import { FC, useState, useRef, useEffect } from 'react' +import React, { FC, useState, useRef, useEffect } from 'react' import {BsYoutube , BsPen} from 'react-icons/bs' import {AiOutlineRead} from 'react-icons/ai' import{MdArticle} from 'react-icons/md' import { CopyToClipboard } from 'components/CopyToClipboard/CopyToClipboard' import Share from 'components/Share/Share' import type { IData } from 'types' +import { collection, doc,where,query,getDocs, setDoc,getDoc } from 'firebase/firestore' +import { onAuthStateChanged } from 'firebase/auth' +import {db,auth} from '../../lib/firebase-config' +import { Timestamp } from 'firebase/firestore' +import Image from 'next/image' +import { toast } from 'react-toastify' +import 'react-toastify/dist/ReactToastify.css'; interface CardProps { - data: IData + data: IData, } export const Card: FC = ({ data }) => { @@ -15,6 +22,114 @@ export const Card: FC = ({ data }) => { const descriptionRef = useRef(null) const [isOverflow, setIsOverflow] = useState(false) const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/ + const id = data.url.replace(/[^\w\s]/gi, ''); + + const [upvoteCount,setUpvoteCount] = useState(0) + const [isUpvoted,setIsUpvoted] = useState(false); + const timestamp = Timestamp.fromDate(new Date()) + const date = timestamp.toDate() + const [user,setUser] = useState(null); + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (user) => { + if (user) { + setUser(user.displayName); + } else { + setUser(null); + } + console.log('Authentication state changed:', user); + }); + + return () => unsubscribe(); + }, []); + const docRef = doc(db, 'resources', id) + const save = async()=>{ + await setDoc(docRef, { + name: name, + description: description, + url: url, + upvotedBy: [user], + upvotes: upvoteCount, + created: date, + }, + { merge: true } + ) + } + + const addUserToAssetBookmark = async () => { + try { + const subcollectionRef = collection(db, 'resources') + const assetQuery = query(subcollectionRef, where('name', '==', data.name)) + const assetQuerySnapshot = await getDocs(assetQuery) + + if (assetQuerySnapshot.empty) { + console.log('Asset not found'); + return; + } + + const assetDocSnapshot = assetQuerySnapshot.docs[0]; + const assetDocRef = doc(db, 'resources', data.name); + const assetData = assetDocSnapshot.data(); + const upvotes = assetData.upvotes || {}; + const userUid = auth.currentUser? auth.currentUser.uid : null; + + if (userUid && upvotes[userUid]) { + // User has already upvoted, so remove their upvote + delete upvotes[userUid]; + } else { + // User has not upvoted, so add their upvote + if(userUid) + { + upvotes[userUid] = true; + } + } + await setDoc(assetDocRef, { + ...assetData, // Keep existing data + upvotes: upvotes, + }); + + await getDoc(assetDocRef); + + const updatedAssetDoc = await getDoc(assetDocRef); + if (!updatedAssetDoc.exists()) { + console.log('Asset document not found'); + return; + } + const updatedUpvotes = updatedAssetDoc.data().upvotes || {}; + const upvoteCount = Object.keys(updatedUpvotes).length; + setUpvoteCount(upvoteCount); + } catch (error) { + console.error('Error adding user to asset upvotes:', error); + } + }; + + const toggleUpvote = () => { + setIsUpvoted(p => !p); + }; + const [errorToastShown, setErrorToastShown] = useState(false); + const handleClick = async(e: React.MouseEvent)=>{ + + const currentUser = auth.currentUser; + if (!currentUser && !errorToastShown) { + console.log('User is not authenticated'); + toast.error('Please Sign In to upvote!!'); + setErrorToastShown(true); + setTimeout(()=>{ + window.location.href = '/'; + },2000) + toggleUpvote(); + } + e.stopPropagation(); + e.preventDefault(); + toggleUpvote(); + save(); + await addUserToAssetBookmark(); + } + /* eslint-disable @typescript-eslint/no-explicit-any */ + function Img({ url }:any) { + return ( + {'altimage'} + ); + } useEffect(() => { if (descriptionRef.current) { @@ -24,7 +139,6 @@ export const Card: FC = ({ data }) => { ) } }, []) - return (
@@ -53,6 +167,10 @@ export const Card: FC = ({ data }) => {

)}
+
+

{upvoteCount}

+ +