- Updated the colors of the application;
- Added the ability for a user to partially update their profile
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
10
src/constants/errors.ts
Normal 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",
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user