- 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?
</span>
<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
</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
</Button>
</div>

View File

@@ -51,10 +51,10 @@ function WordsDrawer({words, isOpen, blankId, previouslySelectedWord, onCancel,
))}
</div>
<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
</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
</Button>
</div>

View File

@@ -94,11 +94,11 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
</div>
<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
</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
</Button>
</div>

View File

@@ -3,26 +3,26 @@ import {ReactNode} from "react";
interface Props {
children: ReactNode;
color?: "orange" | "green" | "blue";
color?: "rose" | "purple" | "red";
variant?: "outline" | "solid";
className?: string;
disabled?: boolean;
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}} = {
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",
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",
},
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",
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",
},
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",
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",

View File

@@ -10,7 +10,7 @@ interface Props {
export default function Navbar({user}: Props) {
return (
<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">
<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">

View File

@@ -99,11 +99,11 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti
</div>
<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
</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
</Button>
</div>

View File

@@ -85,11 +85,11 @@ export default function MatchSentencesSolutions({options, prompt, sentences, use
</div>
<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
</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
</Button>
</div>

View File

@@ -102,11 +102,11 @@ export default function MultipleChoice({prompt, questions, userSolutions, onNext
</div>
<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
</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
</Button>
</div>

View File

@@ -71,11 +71,11 @@ export default function Speaking({title, text, prompts, userSolutions, onNext, o
</div>
<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
</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
</Button>
</div>

View File

@@ -127,11 +127,11 @@ export default function WriteBlanksSolutions({
</div>
<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
</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
</Button>
</div>

View File

@@ -96,11 +96,11 @@ export default function Writing({id, prompt, info, attachment, userSolutions, on
</div>
<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
</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
</Button>
</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>
<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
</Button>
</Link>

View File

@@ -116,7 +116,7 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
</div>
{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
</Button>
)}

View File

@@ -64,7 +64,7 @@ function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: s
</div>
<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
</Button>
</div>
@@ -185,7 +185,7 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
)}
</div>
{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
</Button>
)}

View File

@@ -5,8 +5,13 @@ import {getFirestore, collection, getDocs, getDoc, doc, setDoc} from "firebase/f
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
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 storage = getStorage(app);
const auth = getAuth(app);
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);
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});
}

View File

@@ -133,13 +133,7 @@ export default function Home() {
</section>
<section className="flex flex-col gap-3">
<span className="font-bold text-lg">Bio</span>
<span className="text-mti-gray-taupe">
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>
<span className="text-mti-gray-taupe">{user.bio || "Your bio will appear here..."}</span>
</section>
<section className="flex flex-col gap-3">
<span className="font-bold text-lg">Score History</span>

View File

@@ -83,7 +83,7 @@ export default function Login() {
Forgot Password?
</Link>
</div>
<Button className="mt-8 w-full" color="green" disabled={isLoading}>
<Button className="mt-8 w-full" color="purple" disabled={isLoading}>
{!isLoading && "Login"}
{isLoading && (
<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 {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {useEffect, useState} from "react";
import {ChangeEvent, useEffect, useRef, useState} from "react";
import useStats from "@/hooks/useStats";
import {averageScore, totalExams} from "@/utils/stats";
import useUser from "@/hooks/useUser";
import Sidebar from "@/components/Sidebar";
import Diagnostic from "@/components/Diagnostic";
import {ToastContainer} from "react-toastify";
import {toast, ToastContainer} from "react-toastify";
import {capitalize} from "lodash";
import {Module} from "@/interfaces";
import ProgressBar from "@/components/Low/ProgressBar";
@@ -20,6 +20,8 @@ import Input from "@/components/Low/Input";
import Button from "@/components/Low/Button";
import {useRouter} from "next/router";
import Link from "next/link";
import axios from "axios";
import {ErrorMessage} from "@/constants/errors";
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
@@ -45,8 +47,12 @@ export default function Home() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [profilePicture, setProfilePicture] = useState("");
const profilePictureInput = useRef(null);
const router = useRouter();
const {user} = useUser({redirectTo: "/login"});
@@ -55,9 +61,56 @@ export default function Home() {
setName(user.name);
setEmail(user.email);
setBio(user.bio);
setProfilePicture(user.profilePicture);
}
}, [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 (
<>
<Head>
@@ -106,20 +159,20 @@ export default function Home() {
<Input
label="New Password"
type="password"
name="confirmPassword"
onChange={(e) => setConfirmPassword(e)}
placeholder="Confirm your password"
required
name="newPassword"
onChange={(e) => setNewPassword(e)}
placeholder="Enter your new password (optional)"
/>
</form>
</div>
<div className="flex flex-col gap-3 items-center w-48">
<img
src={user.profilePicture}
alt={user.name}
className="aspect-square h-48 w-48 rounded-full drop-shadow-xl self-end"
/>
<span className="cursor-pointer text-mti-purple-light text-sm">Change picture</span>
<img src={profilePicture} alt={user.name} className="aspect-square h-48 w-48 rounded-full drop-shadow-xl self-end" />
<input type="file" className="hidden" onChange={uploadProfilePicture} accept="image/*" ref={profilePictureInput} />
<span
onClick={() => (profilePictureInput.current as any)?.click()}
className="cursor-pointer text-mti-purple-light text-sm">
Change picture
</span>
</div>
</div>
<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">
<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
</Button>
</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
</Button>
</div>