- Updated the colors of the application;

- Added the ability for a user to partially update their profile
This commit is contained in:
Tiago Ribeiro
2023-07-22 10:11:10 +01:00
parent 6ade34d243
commit 581adbb56e
19 changed files with 152 additions and 52 deletions

View File

@@ -40,10 +40,10 @@ export default function BlankQuestionsModal({isOpen, onClose}: Props) {
Are you sure you want to continue without completing those questions? Are you sure you want to continue without completing those questions?
</span> </span>
<div className="w-full flex justify-between mt-8"> <div className="w-full flex justify-between mt-8">
<Button color="green" onClick={() => onClose(false)} variant="outline" className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => onClose(false)} variant="outline" className="max-w-[200px] self-end w-full">
Go Back Go Back
</Button> </Button>
<Button color="green" onClick={() => onClose(true)} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => onClose(true)} className="max-w-[200px] self-end w-full">
Continue Continue
</Button> </Button>
</div> </div>

View File

@@ -51,10 +51,10 @@ function WordsDrawer({words, isOpen, blankId, previouslySelectedWord, onCancel,
))} ))}
</div> </div>
<div className="flex justify-between w-full"> <div className="flex justify-between w-full">
<Button color="green" variant="outline" className="max-w-[200px] w-full" onClick={onCancel}> <Button color="purple" variant="outline" className="max-w-[200px] w-full" onClick={onCancel}>
Back Back
</Button> </Button>
<Button color="green" className="max-w-[200px] w-full" onClick={() => onAnswer(selectedWord!)} disabled={!selectedWord}> <Button color="purple" className="max-w-[200px] w-full" onClick={() => onAnswer(selectedWord!)} disabled={!selectedWord}>
Confirm Confirm
</Button> </Button>
</div> </div>

View File

@@ -94,11 +94,11 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button color="green" variant="outline" onClick={back} className="max-w-[200px] w-full"> <Button color="purple" variant="outline" onClick={back} className="max-w-[200px] w-full">
Back Back
</Button> </Button>
<Button color="green" onClick={next} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>
</div> </div>

View File

@@ -3,26 +3,26 @@ import {ReactNode} from "react";
interface Props { interface Props {
children: ReactNode; children: ReactNode;
color?: "orange" | "green" | "blue"; color?: "rose" | "purple" | "red";
variant?: "outline" | "solid"; variant?: "outline" | "solid";
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
onClick?: () => void; onClick?: () => void;
} }
export default function Button({color = "green", variant = "solid", disabled = false, className, children, onClick}: Props) { export default function Button({color = "purple", variant = "solid", disabled = false, className, children, onClick}: Props) {
const colorClassNames: {[key in typeof color]: {[key in typeof variant]: string}} = { const colorClassNames: {[key in typeof color]: {[key in typeof variant]: string}} = {
green: { purple: {
solid: "bg-mti-purple-light text-white hover:bg-mti-purple disabled:text-mti-purple disabled:bg-mti-purple-ultralight selection:bg-mti-purple-dark", solid: "bg-mti-purple-light text-white hover:bg-mti-purple disabled:text-mti-purple disabled:bg-mti-purple-ultralight selection:bg-mti-purple-dark",
outline: outline:
"bg-transparent text-mti-purple-light border border-mti-purple-light hover:bg-mti-purple-light disabled:text-mti-purple disabled:bg-mti-purple-ultralight selection:bg-mti-purple-dark hover:text-white selection:text-white", "bg-transparent text-mti-purple-light border border-mti-purple-light hover:bg-mti-purple-light disabled:text-mti-purple disabled:bg-mti-purple-ultralight selection:bg-mti-purple-dark hover:text-white selection:text-white",
}, },
blue: { red: {
solid: "bg-mti-red-light text-white hover:bg-mti-red disabled:text-mti-red disabled:bg-mti-red-ultralight selection:bg-mti-red-dark", solid: "bg-mti-red-light text-white hover:bg-mti-red disabled:text-mti-red disabled:bg-mti-red-ultralight selection:bg-mti-red-dark",
outline: outline:
"bg-transparent text-mti-red-light border border-mti-red-light hover:bg-mti-red-light disabled:text-mti-red disabled:bg-mti-red-ultralight selection:bg-mti-red-dark hover:text-white selection:text-white", "bg-transparent text-mti-red-light border border-mti-red-light hover:bg-mti-red-light disabled:text-mti-red disabled:bg-mti-red-ultralight selection:bg-mti-red-dark hover:text-white selection:text-white",
}, },
orange: { rose: {
solid: "bg-mti-orange-light text-white hover:bg-mti-orange disabled:text-mti-orange disabled:bg-mti-orange-ultralight selection:bg-mti-orange-dark", solid: "bg-mti-orange-light text-white hover:bg-mti-orange disabled:text-mti-orange disabled:bg-mti-orange-ultralight selection:bg-mti-orange-dark",
outline: outline:
"bg-transparent text-mti-orange-light border border-mti-orange-light hover:bg-mti-orange-light disabled:text-mti-orange disabled:bg-mti-orange-ultralight selection:bg-mti-orange-dark hover:text-white selection:text-white", "bg-transparent text-mti-orange-light border border-mti-orange-light hover:bg-mti-orange-light disabled:text-mti-orange disabled:bg-mti-orange-ultralight selection:bg-mti-orange-dark hover:text-white selection:text-white",

View File

@@ -10,7 +10,7 @@ interface Props {
export default function Navbar({user}: Props) { export default function Navbar({user}: Props) {
return ( return (
<header className="w-full bg-transparent py-4 gap-2 flex items-center"> <header className="w-full bg-transparent py-4 gap-2 flex items-center">
<h1 className="font-bold text-2xl w-1/6 px-8">eCrop</h1> <h1 className="font-bold text-2xl w-1/6 px-8">EnCoach</h1>
<div className="flex justify-between w-5/6 mr-8"> <div className="flex justify-between w-5/6 mr-8">
<input type="text" placeholder="Search..." className="rounded-full py-4 px-6 border border-mti-gray-platinum outline-none" /> <input type="text" placeholder="Search..." className="rounded-full py-4 px-6 border border-mti-gray-platinum outline-none" />
<Link href="/profile" className="flex gap-3 items-center justify-end"> <Link href="/profile" className="flex gap-3 items-center justify-end">

View File

@@ -99,11 +99,11 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button color="green" variant="outline" onClick={onBack} className="max-w-[200px] w-full"> <Button color="purple" variant="outline" onClick={onBack} className="max-w-[200px] w-full">
Back Back
</Button> </Button>
<Button color="green" onClick={() => onNext()} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>
</div> </div>

View File

@@ -85,11 +85,11 @@ export default function MatchSentencesSolutions({options, prompt, sentences, use
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button color="green" variant="outline" onClick={() => onBack()} className="max-w-[200px] w-full"> <Button color="purple" variant="outline" onClick={() => onBack()} className="max-w-[200px] w-full">
Back Back
</Button> </Button>
<Button color="green" onClick={() => onNext()} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>
</div> </div>

View File

@@ -102,11 +102,11 @@ export default function MultipleChoice({prompt, questions, userSolutions, onNext
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button color="green" variant="outline" onClick={back} className="max-w-[200px] w-full"> <Button color="purple" variant="outline" onClick={back} className="max-w-[200px] w-full">
Back Back
</Button> </Button>
<Button color="green" onClick={next} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>
</div> </div>

View File

@@ -71,11 +71,11 @@ export default function Speaking({title, text, prompts, userSolutions, onNext, o
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button color="green" variant="outline" onClick={onBack} className="max-w-[200px] w-full"> <Button color="purple" variant="outline" onClick={onBack} className="max-w-[200px] w-full">
Back Back
</Button> </Button>
<Button color="green" onClick={() => onNext()} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>
</div> </div>

View File

@@ -127,11 +127,11 @@ export default function WriteBlanksSolutions({
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button color="green" variant="outline" onClick={() => onBack()} className="max-w-[200px] w-full"> <Button color="purple" variant="outline" onClick={() => onBack()} className="max-w-[200px] w-full">
Back Back
</Button> </Button>
<Button color="green" onClick={() => onNext()} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>
</div> </div>

View File

@@ -96,11 +96,11 @@ export default function Writing({id, prompt, info, attachment, userSolutions, on
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button color="green" variant="outline" onClick={onBack} className="max-w-[200px] w-full"> <Button color="purple" variant="outline" onClick={onBack} className="max-w-[200px] w-full">
Back Back
</Button> </Button>
<Button color="green" onClick={() => onNext()} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>
</div> </div>

10
src/constants/errors.ts Normal file
View File

@@ -0,0 +1,10 @@
export type Error = "E001" | "E002";
export interface ErrorMessage {
error: Error;
message: string;
}
export const errorMessages: {[key in Error]: string} = {
E001: "Wrong password!",
E002: "Invalid e-mail",
};

View File

@@ -191,7 +191,7 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
</div> </div>
<Link href="/" className="max-w-[200px] w-full self-end"> <Link href="/" className="max-w-[200px] w-full self-end">
<Button color="green" className="max-w-[200px] self-end w-full"> <Button color="purple" className="max-w-[200px] self-end w-full">
Dashboard Dashboard
</Button> </Button>
</Link> </Link>

View File

@@ -116,7 +116,7 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
</div> </div>
{exerciseIndex === -1 && ( {exerciseIndex === -1 && (
<Button color="green" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full justify-self-end"> <Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full justify-self-end">
Start now Start now
</Button> </Button>
)} )}

View File

@@ -64,7 +64,7 @@ function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: s
</div> </div>
<div className="absolute bottom-8 right-8 max-w-[200px] self-end w-full"> <div className="absolute bottom-8 right-8 max-w-[200px] self-end w-full">
<Button color="green" variant="outline" className="max-w-[200px] self-end w-full" onClick={onClose}> <Button color="purple" variant="outline" className="max-w-[200px] self-end w-full" onClick={onClose}>
Close Close
</Button> </Button>
</div> </div>
@@ -185,7 +185,7 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
)} )}
</div> </div>
{exerciseIndex === -1 && ( {exerciseIndex === -1 && (
<Button color="green" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
Start now Start now
</Button> </Button>
)} )}

View File

@@ -5,8 +5,13 @@ import {getFirestore, collection, getDocs, getDoc, doc, setDoc} from "firebase/f
import {withIronSessionApiRoute} from "iron-session/next"; import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import {sessionOptions} from "@/lib/session";
import {User} from "@/interfaces/user"; import {User} from "@/interfaces/user";
import {getDownloadURL, getStorage, ref, uploadBytes} from "firebase/storage";
import {getAuth, signInWithEmailAndPassword, updateEmail, updatePassword} from "firebase/auth";
import {errorMessages} from "@/constants/errors";
const db = getFirestore(app); const db = getFirestore(app);
const storage = getStorage(app);
const auth = getAuth(app);
export default withIronSessionApiRoute(handler, sessionOptions); export default withIronSessionApiRoute(handler, sessionOptions);
@@ -17,7 +22,45 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} }
const userRef = doc(db, "users", req.session.user.id); const userRef = doc(db, "users", req.session.user.id);
await setDoc(userRef, req.body, {merge: true}); const updatedUser = req.body as User & {password?: string; newPassword?: string};
if (updatedUser.profilePicture && updatedUser.profilePicture !== req.session.user.profilePicture) {
const profilePictureFiletype = updatedUser.profilePicture.split(";")[0].split("/")[1];
const profilePictureRef = ref(storage, `profile_pictures/${req.session.user.id}.${profilePictureFiletype}`);
const pictureBytes = Buffer.from(updatedUser.profilePicture, "base64url");
const pictureSnapshot = await uploadBytes(profilePictureRef, pictureBytes);
const pictureReference = ref(storage, pictureSnapshot.metadata.fullPath);
updatedUser.profilePicture = await getDownloadURL(pictureReference);
}
if (updatedUser.newPassword && updatedUser.password) {
try {
const credential = await signInWithEmailAndPassword(auth, req.session.user.email, updatedUser.password);
await updatePassword(credential.user, updatedUser.newPassword);
} catch {
res.status(400).json({error: "E001", message: errorMessages.E001});
return;
}
}
if (updatedUser.email !== req.session.user.email && updatedUser.password) {
try {
const credential = await signInWithEmailAndPassword(auth, req.session.user.email, updatedUser.password);
await updateEmail(credential.user, updatedUser.email);
} catch {
res.status(400).json({error: "E002", message: errorMessages.E002});
return;
}
}
delete updatedUser.password;
delete updatedUser.newPassword;
await setDoc(userRef, updatedUser, {merge: true});
req.session.user = {...updatedUser, id: req.session.user.id};
await req.session.save();
res.status(200).json({ok: true}); res.status(200).json({ok: true});
} }

View File

@@ -133,13 +133,7 @@ export default function Home() {
</section> </section>
<section className="flex flex-col gap-3"> <section className="flex flex-col gap-3">
<span className="font-bold text-lg">Bio</span> <span className="font-bold text-lg">Bio</span>
<span className="text-mti-gray-taupe"> <span className="text-mti-gray-taupe">{user.bio || "Your bio will appear here..."}</span>
Patricia Smith is a dedicated and enthusiastic student. Her passion for knowledge drives her to constantly seek new
academic challenges. She is recognized for her exemplary work ethic, active participation in the classroom, and commitment
to helping her peers. Her insatiable curiosity has led her to explore a wide range of areas of study, making her a
versatile and adaptable learner. Patricia is a true academic leader, inspiring other students to pursue their own
educational goals.
</span>
</section> </section>
<section className="flex flex-col gap-3"> <section className="flex flex-col gap-3">
<span className="font-bold text-lg">Score History</span> <span className="font-bold text-lg">Score History</span>

View File

@@ -83,7 +83,7 @@ export default function Login() {
Forgot Password? Forgot Password?
</Link> </Link>
</div> </div>
<Button className="mt-8 w-full" color="green" disabled={isLoading}> <Button className="mt-8 w-full" color="purple" disabled={isLoading}>
{!isLoading && "Login"} {!isLoading && "Login"}
{isLoading && ( {isLoading && (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">

View File

@@ -4,13 +4,13 @@ import Navbar from "@/components/Navbar";
import {BsFileEarmarkText, BsPencil, BsStar, BsBook, BsHeadphones, BsPen, BsMegaphone, BsArrowRepeat} from "react-icons/bs"; import {BsFileEarmarkText, BsPencil, BsStar, BsBook, BsHeadphones, BsPen, BsMegaphone, BsArrowRepeat} from "react-icons/bs";
import {withIronSessionSsr} from "iron-session/next"; import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import {sessionOptions} from "@/lib/session";
import {useEffect, useState} from "react"; import {ChangeEvent, useEffect, useRef, useState} from "react";
import useStats from "@/hooks/useStats"; import useStats from "@/hooks/useStats";
import {averageScore, totalExams} from "@/utils/stats"; import {averageScore, totalExams} from "@/utils/stats";
import useUser from "@/hooks/useUser"; import useUser from "@/hooks/useUser";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
import Diagnostic from "@/components/Diagnostic"; import Diagnostic from "@/components/Diagnostic";
import {ToastContainer} from "react-toastify"; import {toast, ToastContainer} from "react-toastify";
import {capitalize} from "lodash"; import {capitalize} from "lodash";
import {Module} from "@/interfaces"; import {Module} from "@/interfaces";
import ProgressBar from "@/components/Low/ProgressBar"; import ProgressBar from "@/components/Low/ProgressBar";
@@ -20,6 +20,8 @@ import Input from "@/components/Low/Input";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import {useRouter} from "next/router"; import {useRouter} from "next/router";
import Link from "next/link"; import Link from "next/link";
import axios from "axios";
import {ErrorMessage} from "@/constants/errors";
export const getServerSideProps = withIronSessionSsr(({req, res}) => { export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
@@ -45,8 +47,12 @@ export default function Home() {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [profilePicture, setProfilePicture] = useState("");
const profilePictureInput = useRef(null);
const router = useRouter();
const {user} = useUser({redirectTo: "/login"}); const {user} = useUser({redirectTo: "/login"});
@@ -55,9 +61,56 @@ export default function Home() {
setName(user.name); setName(user.name);
setEmail(user.email); setEmail(user.email);
setBio(user.bio); setBio(user.bio);
setProfilePicture(user.profilePicture);
} }
}, [user]); }, [user]);
const convertBase64 = (file: File) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = () => {
resolve(fileReader.result);
};
fileReader.onerror = (error) => {
reject(error);
};
});
};
const uploadProfilePicture = async (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files[0]) {
const picture = event.target.files[0];
const base64 = await convertBase64(picture);
setProfilePicture(base64 as string);
}
};
const updateUser = async () => {
setIsLoading(true);
if (email !== user?.email && !password) {
toast.error("To update your e-mail you need to input your password!");
setIsLoading(false);
return;
}
if (newPassword && !password) {
toast.error("To update your password you need to input your current one!");
setIsLoading(false);
return;
}
const request = await axios.post("/api/users/update", {bio, name, email, password, newPassword});
if (request.status === 200) {
toast.success("Your profile has been updated!");
setTimeout(() => router.reload(), 800);
return;
}
toast.error((request.data as ErrorMessage).message);
setIsLoading(false);
};
return ( return (
<> <>
<Head> <Head>
@@ -106,20 +159,20 @@ export default function Home() {
<Input <Input
label="New Password" label="New Password"
type="password" type="password"
name="confirmPassword" name="newPassword"
onChange={(e) => setConfirmPassword(e)} onChange={(e) => setNewPassword(e)}
placeholder="Confirm your password" placeholder="Enter your new password (optional)"
required
/> />
</form> </form>
</div> </div>
<div className="flex flex-col gap-3 items-center w-48"> <div className="flex flex-col gap-3 items-center w-48">
<img <img src={profilePicture} alt={user.name} className="aspect-square h-48 w-48 rounded-full drop-shadow-xl self-end" />
src={user.profilePicture} <input type="file" className="hidden" onChange={uploadProfilePicture} accept="image/*" ref={profilePictureInput} />
alt={user.name} <span
className="aspect-square h-48 w-48 rounded-full drop-shadow-xl self-end" onClick={() => (profilePictureInput.current as any)?.click()}
/> className="cursor-pointer text-mti-purple-light text-sm">
<span className="cursor-pointer text-mti-purple-light text-sm">Change picture</span> Change picture
</span>
</div> </div>
</div> </div>
<div className="flex flex-col gap-4 mt-8 mb-20"> <div className="flex flex-col gap-4 mt-8 mb-20">
@@ -134,11 +187,11 @@ export default function Home() {
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Link href="/" className="max-w-[200px] self-end w-full"> <Link href="/" className="max-w-[200px] self-end w-full">
<Button color="green" variant="outline" className="max-w-[200px] self-end w-full"> <Button color="purple" variant="outline" className="max-w-[200px] self-end w-full">
Back Back
</Button> </Button>
</Link> </Link>
<Button color="green" className="max-w-[200px] self-end w-full"> <Button color="purple" className="max-w-[200px] self-end w-full" onClick={updateUser} disabled={isLoading}>
Save Changes Save Changes
</Button> </Button>
</div> </div>