diff --git a/.vscode/settings.json b/.vscode/settings.json index cf555c5..414859b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ "tools", "partnerPage", "footer", - "auth" + "auth", + "voting" ] } \ No newline at end of file diff --git a/components/CategoryMain.tsx b/components/CategoryMain.tsx index 8397e7b..6e85a05 100644 --- a/components/CategoryMain.tsx +++ b/components/CategoryMain.tsx @@ -1,12 +1,37 @@ +import { useUser } from "@supabase/auth-helpers-react"; import { Table } from "flowbite-react"; +import toast, { Toaster } from "react-hot-toast"; +import { supabase } from "../util/supabase"; import EmbeddedSearchbar from "./EmbeddedSearchbar"; import ToolTableRow from "./ToolTableRow"; export default function CategoryMain(props: any) { + const { user, error } = useUser(); + const supabaseCallUser = supabase.auth.user() + + const userInfo = user || supabaseCallUser || null + + + const notifications = { + "noLoginVote": () => toast.error('You need to login first before voting!', {icon: "🙈", position: "bottom-center", duration: 3000}), + "voteAdded": (promise:Promise<{}>) => toast.promise(promise, { + loading: 'Adding vote...', + success: Voted!, + error: Sorry, an error happened. Please try again later. + }, {position: "bottom-center", duration: 2000}).then((r) => r).catch((error) => console.error(error)), + "voteRemoved": (promise:Promise<{}>) => toast.promise(promise, { + loading: 'Remove vote...', + success: Vote removed!, + error: Sorry, an error happened. Please try again later. + }, {position: "bottom-center", duration: 2000}).then((r) => r).catch((error) => console.error(error)), + "voteError": () => toast.error('An error occurred while trying to vote / unvote!', {position: "bottom-center", duration: 3000}), + } + // Show the tool page depending on the id of the tool return ( + @@ -38,7 +63,7 @@ export default function CategoryMain(props: any) { { props.categoryResults.map((tool: any, index: number) => { return ( - + ) }) } diff --git a/components/EmbeddedSearchbar.tsx b/components/EmbeddedSearchbar.tsx index 1141d50..54eb714 100644 --- a/components/EmbeddedSearchbar.tsx +++ b/components/EmbeddedSearchbar.tsx @@ -3,6 +3,9 @@ import Link from 'next/link' import axios from 'axios' import { useState } from 'react'; import ToolTableRow from './ToolTableRow'; +import toast from 'react-hot-toast'; +import { supabase } from '../util/supabase'; +import { useUser } from '@supabase/auth-helpers-react'; export default function EmbeddedSearchbar(props: any) { @@ -10,6 +13,11 @@ export default function EmbeddedSearchbar(props: any) { const [results, setResults] = useState([]) const [tableVisible, setTableVisibility] = useState(false) + const { user, error } = useUser(); + const supabaseCallUser = supabase.auth.user() + + const userInfo = user || supabaseCallUser || null + const showSearchResults = async (searchTerm: string) => { if (searchTerm.length > 3){ @@ -31,6 +39,21 @@ export default function EmbeddedSearchbar(props: any) { } + const notifications = { + "noLoginVote": () => toast.error('You need to login first before voting!', {icon: "🙈", position: "bottom-center", duration: 3000}), + "voteAdded": (promise:Promise<{}>) => toast.promise(promise, { + loading: 'Adding vote...', + success: Voted!, + error: Sorry, an error happened. Please try again later. + }, {position: "bottom-center", duration: 2000}).then((r) => r).catch((error) => console.error(error)), + "voteRemoved": (promise:Promise<{}>) => toast.promise(promise, { + loading: 'Remove vote...', + success: Vote removed!, + error: Sorry, an error happened. Please try again later. + }, {position: "bottom-center", duration: 2000}).then((r) => r).catch((error) => console.error(error)), + "voteError": () => toast.error('An error occurred while trying to vote / unvote!', {position: "bottom-center", duration: 3000}), + } + return ( 0 ? ( results.map((row: any, index: number) => { return ( - + ) }) ) : ( diff --git a/components/Header.tsx b/components/Header.tsx index c1f3a0b..3eca393 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,14 +1,37 @@ -import {useState} from "react"; import Image from "next/image" import Link from "next/link"; import Logo from "../assets/default-monochrome-white.svg" import { SiDiscord, SiTwitter } from "react-icons/si"; -import { Button } from "flowbite-react"; +import { Avatar, Button, Dropdown, Spinner } from "flowbite-react"; import LoginModal from "./LoginModal"; +import { useUser } from '@supabase/auth-helpers-react'; +import { supabase } from "../util/supabase"; +import { Suspense, useEffect, useState } from 'react'; -export default function Header() { +export default function Header(props:any) { + const [navbarOpen, setNavbarOpen] = useState(false); const [showLoginModal, setShowLoginModal] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + const { user, error } = useUser(); + const [ userData, setUserData] : any = useState({username: "loading..."}); + + const supabaseCallUser = supabase.auth.user() + + useEffect(() => { + setIsLoading(false); + async function loadData(user:any) { + const { data } : any = await supabase.from('profiles').select('*').eq("user_id", user?.id); + setUserData(data[0]); + } + if (user) { + loadData(user); + } + if(supabaseCallUser){ + loadData(supabaseCallUser) + } + }, [user]); return ( @@ -103,12 +126,44 @@ export default function Header() { > + { isLoading ? () : ( + }> + { supabaseCallUser ? ( + + ) : {supabaseCallUser.user_metadata.full_name || userData.username || supabaseCallUser.email} }> + + + {supabaseCallUser.user_metadata.full_name || userData.username || supabaseCallUser.email } + + + {supabaseCallUser.email} + + + + + Settings + + + {supabase.auth.signOut(); window.location.href = "/";}}> + Sign out + + + + ) : ( {setShowLoginModal(true)}} > Sign In + )} + + )} diff --git a/components/LoginModal.tsx b/components/LoginModal.tsx index 8fe772f..367935c 100644 --- a/components/LoginModal.tsx +++ b/components/LoginModal.tsx @@ -5,132 +5,15 @@ import { supabase } from "../util/supabase"; export default function LoginModal(props: any){ - const [showRegisterContent, setShowRegisterContent] = useState(false) const [showAlert, setShowAlert] = useState(false) const [showSuccessAlert, setShowSuccessAlert] = useState(false) - const [alertMessage, setAlertMessage] = useState("") - const initialLoginData = Object.freeze({ - email: "", - password: "" - }); - - const initialRegisterData = Object.freeze({ - registerUsername: "", - registerEmail: "", - registerRepeatEmail: "", - registerPassword: "" - }); - - const [formData, updateFormData] = useState(initialLoginData); - const [formRegisterData, updateRegisterFormData] = useState(initialRegisterData); - - const handleChange = (e:any) => { - updateFormData({ - ...formData, - // Trimming any whitespace - [e.target.name]: e.target.value.trim() - }); - }; - - const handleRegisterChange = (e:any) => { - updateRegisterFormData({ - ...formRegisterData, - // Trimming any whitespace - [e.target.name]: e.target.value.trim() - }); - }; - - async function recoverPassword() { - if(formData.email.trim() === ""){ - setShowAlert(true) - setAlertMessage("Please enter an email address in the email field") - }else if(!formData.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)){ - setShowAlert(true) - setAlertMessage("Please enter a valid email address") - }else{ - - const { data, error } = await supabase.auth.api.resetPasswordForEmail(formData.email); - - if(error){ - setShowAlert(true) - setAlertMessage(error.message) - }else{ - setShowAlert(false) - setShowSuccessAlert(true) - setAlertMessage("Password reset email sent. Please check your email.") - // TODO: Create a basic site for this and link to it in the email - } - - } - } - - async function signInWithEmail() { - - if(formData.email.trim() === ""){ - setShowAlert(true) - setAlertMessage("Please enter an email address") - }else if(formData.password.trim() === ""){ - setShowAlert(true) - setAlertMessage("Please enter a password") - }else{ - const { user, error } = await supabase.auth.signIn({ - email: formData.email, - password: formData.password, - }) - - if(error){ - setShowAlert(true) - setAlertMessage(error.message) - } - - if(user){ - setShowAlert(false) - setShowSuccessAlert(true) - setAlertMessage("Successfully signed in") - window.location.reload() - } - } - } - - async function registerWithEmail() { - const { user, error } = await supabase.auth.signUp({ - email: formRegisterData.registerEmail, - password: formRegisterData.registerPassword, - }) - - if(error){ - setShowAlert(true) - setAlertMessage(error.message) - } - - if(user){ - const {data, error: error2} = await supabase.from("profiles").insert({ - username: formRegisterData.registerUsername, - user_id: user.id - }) - - if(error2){ - setShowAlert(true) - setAlertMessage(error2.message) - } - - if(data){ - setShowAlert(false) - setShowSuccessAlert(true) - setAlertMessage("Successfully registered. Please check your email to verify your account.") - } - } - - } - async function signInWithGithub() { const { user, session, error } = await supabase.auth.signIn({ provider: 'github', }) } - return ( @@ -144,7 +27,7 @@ export default function LoginModal(props: any){ - { showRegisterContent ? "Register" : "Sign in" } to toolDB + Sign in to toolDB { showAlert ? ( {alertMessage} ) : null } - { !showRegisterContent ? ( - <> - - - - E-Mail - - - - - - Password - - - - - recoverPassword()} - className="text-sm text-blue-700 hover:underline dark:text-blue-500" - > - Lost Password? - - - - { - e.preventDefault() - if(formData.email.trim() === ""){ - setShowAlert(true) - setAlertMessage("Please enter an email address") - } - else if(formData.password.trim() === ""){ - setShowAlert(true) - setAlertMessage("Please enter a password") - } - else{ - // Check if email is valid and could be a real email adress - if(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)){ - await signInWithEmail() - }else{ - setShowAlert(true) - setAlertMessage("Please enter a valid email address") - } - } - }} - type="submit" - > - Log in to your account - - - - Not registered?{' '} - {setShowRegisterContent(true); setShowAlert(false)}} - className="text-blue-700 hover:underline dark:text-blue-500 cursor-pointer" - > - Create account - - - - > - ) : ( - <> - - - - Username - - - - - - E-Mail - - - - - - Repeat E-Mail - - - - - - Password - - - - - { - e.preventDefault() - if(formRegisterData.registerEmail.trim() === ""){ - setShowAlert(true) - setAlertMessage("Please enter an email address") - } - else if(formRegisterData.registerRepeatEmail.trim() === ""){ - setShowAlert(true) - setAlertMessage("Please repeat your email address") - } - else if(formRegisterData.registerUsername.trim() === ""){ - setShowAlert(true) - setAlertMessage("Please enter a username") - } - else if(formRegisterData.registerUsername.trim().length < 5){ - setShowAlert(true) - setAlertMessage("Username must be at least 5 characters long") - } - else if(formRegisterData.registerPassword.trim() === ""){ - setShowAlert(true) - setAlertMessage("Please enter a password") - } - else if(formRegisterData.registerPassword.trim().length < 8){ - setShowAlert(true) - setAlertMessage("Password must be at least 8 characters long") - } - else if(formRegisterData.registerEmail !== formRegisterData.registerRepeatEmail){ - setShowAlert(true) - setAlertMessage("Your email addresses do not match") - } - else{ - // Check if email is valid and could be a real email adress - if(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formRegisterData.registerEmail)){ - await registerWithEmail() - }else{ - setShowAlert(true) - setAlertMessage("Please enter a valid email address") - } - } - } - } - > - Register - - - - Already registered?{' '} - {setShowRegisterContent(false); setShowAlert(false)}} - className="text-blue-700 hover:underline dark:text-blue-500 cursor-pointer" - > - Log in - - - - > - )} - - or - - signInWithGithub()}> Sign in with GitHub + + await signInWithGithub()}> Sign in with GitHub + diff --git a/components/Main.tsx b/components/Main.tsx index 23412b3..706c16d 100644 --- a/components/Main.tsx +++ b/components/Main.tsx @@ -4,6 +4,7 @@ import { Card, Progress, Spinner } from 'flowbite-react'; import Link from 'next/link'; import useSWR from 'swr'; import axios from 'axios'; +import { Toaster } from 'react-hot-toast'; const fetcher = (url:any) => axios.get(url).then(res => res.data) @@ -35,6 +36,7 @@ export default function Main(props: any) { return ( + Search the tool you need for your project diff --git a/components/ToolMain.tsx b/components/ToolMain.tsx index 03b4ddd..b6990d6 100644 --- a/components/ToolMain.tsx +++ b/components/ToolMain.tsx @@ -1,7 +1,7 @@ import { Avatar, Badge, Button, Carousel, Modal, Spinner, Tooltip } from "flowbite-react"; import Link from "next/link"; -import { Suspense, useState } from "react"; -import { FaDiscord, FaExclamationCircle, FaGithub, FaLink, FaStar, FaTwitter } from "react-icons/fa"; +import { Suspense, useEffect, useState } from "react"; +import { FaArrowUp, FaDiscord, FaExclamationCircle, FaGithub, FaLink, FaRegTimesCircle, FaStar, FaTwitter } from "react-icons/fa"; import { RiFilePaper2Fill } from "react-icons/ri"; import { CgGitFork } from "react-icons/cg"; import EmbeddedSearchbar from "./EmbeddedSearchbar"; @@ -11,15 +11,85 @@ import Vibrant from "node-vibrant" import ReactMarkdown from 'react-markdown' import gfm from 'remark-gfm' import rehypeRaw from 'rehype-raw' +import { Toaster } from "react-hot-toast"; +import axios from "axios"; +import useSWR from 'swr'; + +const fetcher = (url:any) => axios.get(url).then(res => res.data) export default function ToolMain(props: any) { const [vibrantColors, setVibrantColors] = useState([]); const [modals, setModal] = useState([]); + const [toolVotes, setVotes] = useState(0) + const [isLoading, setIsLoading] = useState(true) + const [isVoted, setIsVoted] = useState(false); + const userInfo = props.userInfo + + const { data: votes, error: voteCountError }: any = useSWR(`/api/getVotes/${props.toolData.id}`, fetcher) + + useEffect(() => { + if(votes){ + if(userInfo){ + axios.get(`/api/tools/vote?id=${props.toolData.id}`).then(res => { + if(res.data.voted){ + setIsVoted(true) + setVotes(votes.votes) + setIsLoading(false) + }else{ + setIsVoted(false) + setVotes(votes.votes) + setIsLoading(false) + } + }).catch(err => { + console.log(err) + setVotes(votes.votes) + setIsLoading(false) + }) + }else{ + setVotes(votes.votes) + setIsLoading(false) + } + } + }, [votes, userInfo, props.toolData.id]) + + async function handleVote (e:any) { + if(votes && userInfo && !isLoading && !voteCountError){ + const oldVotes = toolVotes + if(isVoted){ + try { + setVotes(toolVotes - 1) + setIsVoted(false) + const removeVote = axios.post(`/api/tools/vote?id=${props.toolData.id}`, { + userId: userInfo.id, + }) + props.notifications.voteRemoved(removeVote); + } catch (error) { + setVotes(oldVotes) + props.notifications.voteError(); + } + }else{ + try { + setVotes(toolVotes + 1) + setIsVoted(true) + const addVote = axios.post(`/api/tools/vote?id=${props.toolData.id}`, { + userId: userInfo.id, + }) + props.notifications.voteAdded(addVote); + } catch (error) { + setVotes(oldVotes) + props.notifications.voteError(); + } + } + }else{ + props.notifications.noLoginVote(); + } + } // Show the tool page depending on the id of the tool return ( + @@ -32,6 +102,10 @@ export default function ToolMain(props: any) { {props.toolData.tool_name} + + + {voteCountError ? : votes && !isLoading ? toolVotes : } + { diff --git a/components/ToolSearchTable.tsx b/components/ToolSearchTable.tsx index 434f349..6452087 100644 --- a/components/ToolSearchTable.tsx +++ b/components/ToolSearchTable.tsx @@ -1,10 +1,13 @@ -import { Button, Pagination, Progress, Spinner, Table, TextInput } from 'flowbite-react' +import { Button, Pagination, Progress, Spinner, Table, TextInput, Toast } from 'flowbite-react' import Link from 'next/link' import axios from 'axios' import { useEffect, useRef, useState } from 'react'; import { FaStar, FaTable } from 'react-icons/fa'; import useSWR from 'swr'; import ToolTableRow from './ToolTableRow'; +import { useUser } from '@supabase/auth-helpers-react'; +import { supabase } from "../util/supabase"; +import toast from 'react-hot-toast'; const fetcher = (url:any) => axios.get(url).then(res => res.data) @@ -12,6 +15,11 @@ export default function ToolSearchTable(props: any) { let maxPages = useRef(0); + const { user, error } = useUser(); + const supabaseCallUser = supabase.auth.user() + + const userInfo = user || supabaseCallUser || null + const [tablePreviewData, setTablePreviewData] = useState([]) const [standardTableViewVisible, setStandardTableViewVisible] = useState(true) const [recentlyAddedTableViewVisible, setRecentlyAddedTableViewVisible] = useState(false) @@ -117,6 +125,21 @@ export default function ToolSearchTable(props: any) { } + const notifications = { + "noLoginVote": () => toast.error('You need to login first before voting!', {icon: "🙈", position: "bottom-center", duration: 3000}), + "voteAdded": (promise:Promise<{}>) => toast.promise(promise, { + loading: 'Adding vote...', + success: Voted!, + error: Sorry, an error happened. Please try again later. + }, {position: "bottom-center", duration: 2000}).then((r) => r).catch((error) => console.error(error)), + "voteRemoved": (promise:Promise<{}>) => toast.promise(promise, { + loading: 'Remove vote...', + success: Vote removed!, + error: Sorry, an error happened. Please try again later. + }, {position: "bottom-center", duration: 2000}).then((r) => r).catch((error) => console.error(error)), + "voteError": () => toast.error('An error occurred while trying to vote / unvote!', {position: "bottom-center", duration: 3000}), + } + return ( <> Search through {maxPages.current || } tools @@ -175,7 +198,7 @@ export default function ToolSearchTable(props: any) { tablePreviewData ? ( tablePreviewData.map((row: any, index: number) => { return ( - + ) }) ) : ( diff --git a/components/ToolTableRow.tsx b/components/ToolTableRow.tsx index 6fae193..7edb523 100644 --- a/components/ToolTableRow.tsx +++ b/components/ToolTableRow.tsx @@ -1,20 +1,90 @@ -import { Avatar, Badge, BadgeColor, Button, Table, Tooltip } from 'flowbite-react' +import axios from 'axios'; +import { Avatar, Badge, BadgeColor, Button, Spinner, Table, Tooltip } from 'flowbite-react' import Link from 'next/link'; -import { FaArrowUp, FaDiscord, FaGithub, FaLink, FaStar, FaTwitter } from 'react-icons/fa'; +import { useEffect, useState } from 'react'; +import { FaArrowUp, FaDiscord, FaGithub, FaLink, FaRegTimesCircle, FaStar, FaTwitter } from 'react-icons/fa'; +import useSWR from 'swr'; + +const fetcher = (url:any) => axios.get(url).then(res => res.data) export default function ToolTableRow(props: any) { const row = props.row; const showCategories = props.showCategories ? true : false; const showSubmittedBy = props.showSubmittedBy; + const userInfo = props.userInfo + + const [toolVotes, setVotes] = useState(0) + const [isLoading, setIsLoading] = useState(true) + const [isVoted, setIsVoted] = useState(false); + + const { data: votes, error: voteCountError }: any = useSWR(`/api/getVotes/${row.id}`, fetcher) + + useEffect(() => { + if(votes){ + if(userInfo){ + axios.get(`/api/tools/vote?id=${row.id}`).then(res => { + if(res.data.voted){ + setIsVoted(true) + setVotes(votes.votes) + setIsLoading(false) + }else{ + setIsVoted(false) + setVotes(votes.votes) + setIsLoading(false) + } + }).catch(err => { + console.log(err) + setVotes(votes.votes) + setIsLoading(false) + }) + }else{ + setVotes(votes.votes) + setIsLoading(false) + } + } + }, [votes, userInfo, row.id]) + + async function handleVote (e:any) { + if(votes && userInfo && !isLoading && !voteCountError){ + const oldVotes = toolVotes + if(isVoted){ + try { + setVotes(toolVotes - 1) + setIsVoted(false) + const removeVote = axios.post(`/api/tools/vote?id=${row.id}`, { + userId: userInfo.id, + }) + props.notifications.voteRemoved(removeVote); + } catch (error) { + setVotes(oldVotes) + props.notifications.voteError(); + } + }else{ + try { + setVotes(toolVotes + 1) + setIsVoted(true) + const addVote = axios.post(`/api/tools/vote?id=${row.id}`, { + userId: userInfo.id, + }) + props.notifications.voteAdded(addVote); + } catch (error) { + setVotes(oldVotes) + props.notifications.voteError(); + } + } + }else{ + props.notifications.noLoginVote(); + } + } return ( - - - {row.upvotes || 0} + + + {voteCountError ? : votes && !isLoading ? toolVotes : } diff --git a/pages/_app.tsx b/pages/_app.tsx index b0ddd95..8bfc68a 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,8 +1,14 @@ import '../styles/globals.css' import type { AppProps } from 'next/app' +import { UserProvider } from '@supabase/auth-helpers-react'; +import { supabase } from '../util/supabase'; function MyApp({ Component, pageProps }: AppProps) { - + return ( + + + + ); } export default MyApp diff --git a/pages/profile/passwordRecovery.tsx b/pages/profile/passwordRecovery.tsx new file mode 100644 index 0000000..ceabbed --- /dev/null +++ b/pages/profile/passwordRecovery.tsx @@ -0,0 +1,230 @@ +import Head from "next/head"; +import Header from "../../components/Header"; +import Footer from "../../components/Footer"; +import { NextSeo } from "next-seo"; +import Script from "next/script"; +import { useEffect, useState } from "react"; +import { supabase } from "../../util/supabase"; +import { Alert, Button, TextInput } from "flowbite-react"; +import toast, { Toaster } from "react-hot-toast"; + +export default function PasswordRecovery(props: any) { + + const initialPasswordRecovery = Object.freeze({ + newPassword: "", + newRepeatPassword: "" + }); + + const [passwordRecoveryData, updatePasswordRecoveryData] = useState(initialPasswordRecovery); + const [submitted, setSubmitted] = useState(true); + const [userData, setUserData] : any = useState({}); + const [accessToken, setAccessToken] = useState(""); + + useEffect(() => { + + const hashValues = window.location.hash.substring(1).split("&").map((param) => param.split("=")); + + let type; + let accessToken; + + for (const [key, value] of hashValues) { + if (key === "type") { + type = value; + } else if (key === "access_token") { + accessToken = value; + } + } + + console.log(type, accessToken); + console.log(hashValues) + + if (type !== "recovery" || !accessToken || typeof accessToken === "object") { + toast.error("Invalid credentials. Redirecting to homepage", { + duration: 2000 + }); + setTimeout(() => { + window.location.href = "/"; + }, 100000); + return; + } + + setAccessToken(accessToken); + + const data = supabase.auth.api.getUser( + accessToken, + ).then((user) => { + setUserData(user); + if(user?.user?.app_metadata.provider === "github"){ + toast.error("Github users cannot change their password. Redirecting to homepage", { + duration: 2000 + }); + setTimeout(() => { + window.location.href = "/"; + }, 1000); + return; + }else{ + setSubmitted(false); + return; + } + }).catch((error) => { + toast.error("An error occured. Redirecting to homepage", { + duration: 2000 + }); + setTimeout(() => { + window.location.href = "/"; + }, 1000); + return; + }) + + }, []); + + const handleChange = (e:any) => { + updatePasswordRecoveryData({ + ...passwordRecoveryData, + // Trimming any whitespace + [e.target.name]: e.target.value.trim() + }); + }; + + const handleSubmit = async (e:any) => { + e.preventDefault(); + + const notification = toast.loading("Changing password"); + + setSubmitted(true); + + try { + if (passwordRecoveryData.newPassword !== passwordRecoveryData.newRepeatPassword) { + toast.error("Passwords do not match", {id: notification, duration: 2000}); + return; + } + + const { error } = await supabase.auth.api.updateUser(accessToken, { password : passwordRecoveryData.newPassword }) + + if (error) { + toast.error(error.message, { + id: notification, + duration: 2000 + }); + } else if (!error) { + toast.success("Password successfully changed. Redirecting to homepage", { + id: notification, + duration: 2000, + }); + setTimeout(() => { + window.location.href = "/"; + }, 2000); + } + + } catch (error) { + toast.error("An error occured. Please request another password recovery attempt. You will be redirected", { + id: notification, + duration: 2000 + }); + setTimeout(() => { + window.location.href = "/"; + }, 4000); + } + } + return ( + + + + + + + + + + + + + + Password Recovery + + + + Password + + + Your new password + + + + + + Repeat your new password + + + + { + !submitted ? ( + + Change password + + ) : ( + null + ) + } + + + + + + + ); +} \ No newline at end of file diff --git a/pages/profile/settings.tsx b/pages/profile/settings.tsx new file mode 100644 index 0000000..217cd60 --- /dev/null +++ b/pages/profile/settings.tsx @@ -0,0 +1,219 @@ +import Head from "next/head"; +import Header from "../../components/Header"; +import Footer from "../../components/Footer"; +import { NextSeo } from "next-seo"; +import Script from "next/script"; +import { withPageAuth } from "@supabase/auth-helpers-nextjs"; +import { useEffect, useState } from "react"; +import { supabase } from "../../util/supabase"; +import { Alert, Button, Label, TextInput } from "flowbite-react"; +import toast, { Toaster } from "react-hot-toast"; +import axios from "axios"; + +export default function Profile(props: any) { + + const { user } = props; + + const supabaseUserData = supabase.auth.user() + + const [supabaseInfo, setSupabaseInfo] = useState({username: "loading..."}) + const [deleteProcess, setDeleteProcess] = useState(0) + + const initialEmailData = Object.freeze({ + newEmail: "", + }); + + const [emailFormData, updateEmailFormData] = useState(initialEmailData); + const [triggeredPasswordChange, setTriggeredPasswordChange] = useState(false); + + useEffect(() => { + + if (supabaseUserData) { + loadData(user) + }else{ + window.location.href = "/" + } + + async function loadData(user:any) { + const { data } : any = await supabase.from('profiles').select('*').eq("user_id", user?.id) + setSupabaseInfo(data[0]) + } + + }, [supabaseUserData]) + + const deleteAccount = async () => { + + const deletion = axios.get("/api/profile/deleteAccount", { + headers: { + "Delete-Account": "true" + } + }) + + toast.promise(deletion, { + loading: 'Removing account...', + success: Removed!, + error: Sorry, an error happened. Please try again later. + }, {position: "bottom-center", duration: 2000}).then(async (r) => { + await supabase.auth.signOut() + window.location.href = "/" + }).catch((error) => console.error(error)) + + } + + const changePassword = async (e:any) => { + if(!triggeredPasswordChange){ + setTriggeredPasswordChange(true) + e.preventDefault() + const { data, error } = await supabase.auth.api.resetPasswordForEmail(user.email) + + if (error) { + toast.error('Sorry, an unexpected error occured. Please try again later.', {position: "bottom-center", duration: 3000}) + }else{ + console.log(data) + toast.success('Password reset link sent to your email!', {position: "bottom-center", duration: 3000}) + } + + }else{ + toast.error('You already made a recovery request.', {position: "bottom-center", duration: 3000}) + } + } + + return ( + + + + + + + + + + + + + + Hello {user.user_metadata.full_name || supabaseInfo.username}{' '}👋 + + + + E-Mail + + + Your current email + + + + + + Your new email + + + + { + user.app_metadata.provider === "github" ? ( + + + You cannot change your settings here, because you are logged in with GitHub. + + + ) : ( + + Change email + + ) + } + + + Password + { + user.app_metadata.provider === "github" ? ( + + + You cannot change your settings here, because you are logged in with GitHub. + + + ) : ( + + Click here to change your password + + ) + } + Delete Account + { + supabaseUserData ? ( + deleteProcess === 0 ? ( + setDeleteProcess(1)}>Delete your account on toolDB + ) : ( + deleteProcess === 1 ? ( + setDeleteProcess(2)}>Confirm deletion + ) : ( + deleteAccount()}>Confirm deletion finally + ) + ) + ) : null + } + + + + + + + ); +} + +export const getServerSideProps = withPageAuth({ redirectTo: '/' }); \ No newline at end of file diff --git a/pages/tool/[id].tsx b/pages/tool/[id].tsx index 12f0e39..1b5ca38 100644 --- a/pages/tool/[id].tsx +++ b/pages/tool/[id].tsx @@ -9,6 +9,9 @@ import { Suspense, useEffect, useState } from "react"; import { Spinner } from "flowbite-react"; import Script from "next/script"; import { server } from '../../config' +import toast from "react-hot-toast"; +import { useUser } from "@supabase/auth-helpers-react"; +import { supabase } from "../../util/supabase"; export default function ToolPage(props: any) { @@ -17,6 +20,10 @@ export default function ToolPage(props: any) { const { id, isFallback } = router.query const [isSSR, setIsSSR] = useState(true); + const { user, error } = useUser(); + const supabaseCallUser = supabase.auth.user() + + const userInfo = user || supabaseCallUser || null useEffect(() => { setIsSSR(false); @@ -32,6 +39,21 @@ export default function ToolPage(props: any) { ); } + const notifications = { + "noLoginVote": () => toast.error('You need to login first before voting!', {icon: "🙈", position: "bottom-center", duration: 3000}), + "voteAdded": (promise:Promise<{}>) => toast.promise(promise, { + loading: 'Adding vote...', + success: Voted!, + error: Sorry, an error happened. Please try again later. + }, {position: "bottom-center", duration: 2000}).then((r) => r).catch((error) => console.error(error)), + "voteRemoved": (promise:Promise<{}>) => toast.promise(promise, { + loading: 'Remove vote...', + success: Vote removed!, + error: Sorry, an error happened. Please try again later. + }, {position: "bottom-center", duration: 2000}).then((r) => r).catch((error) => console.error(error)), + "voteError": () => toast.error('An error occurred while trying to vote / unvote!', {position: "bottom-center", duration: 3000}), + } + return ( }> - + ): (
or