-
-
- Sign in
-
-
- {error && (
-
-
- Error
-
- {error}
-
-
- )}
-
-
-
-
- Don't have an account?{" "}
-
- Sign up
-
-
-
+
+
PeerPrep
+
Temporary landing page with all the links
+
+
+
+
+
+ Forgot password (enter email for link)
+ Reset password (enter password to reset)
+ Profile page
+
+
+ Questions (user facing)
+ Question Repo (CRUD)
)
diff --git a/frontend/app/questions/page.tsx b/frontend/app/questions/page.tsx
deleted file mode 100644
index 3152b62757..0000000000
--- a/frontend/app/questions/page.tsx
+++ /dev/null
@@ -1,360 +0,0 @@
-"use client";
-
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { Badge, BadgeProps } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Card } from "@/components/ui/card";
-import { MultiSelect } from "@/components/ui/multi-select";
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { Flag, MessageSquareText } from "lucide-react";
-import Link from "next/link";
-import React, { useEffect, useState } from "react";
-
-type Question = {
- id: number;
- title: string;
- complexity: string | undefined;
- categories: (string | undefined)[];
- description: string;
- selected: boolean;
-};
-
-const complexityList: Array<{
- value: string;
- label: string;
- badgeVariant: BadgeProps["variant"];
-}> = [
- { value: "easy", label: "Easy", badgeVariant: "easy" },
- { value: "medium", label: "Medium", badgeVariant: "medium" },
- { value: "hard", label: "Hard", badgeVariant: "hard" },
- ];
-
-const categoryList: Array<{
- value: string;
- label: string;
- badgeVariant: BadgeProps["variant"];
-}> = [
- { value: "algorithms", label: "Algorithms", badgeVariant: "category" },
- { value: "arrays", label: "Arrays", badgeVariant: "category" },
- {
- value: "bitmanipulation",
- label: "Bit Manipulation",
- badgeVariant: "category",
- },
- { value: "brainteaser", label: "Brainteaser", badgeVariant: "category" },
- { value: "databases", label: "Databases", badgeVariant: "category" },
- { value: "datastructures", label: "Data Structures", badgeVariant: "category" },
- { value: "recursion", label: "Recursion", badgeVariant: "category" },
- { value: "strings", label: "Strings", badgeVariant: "category" },
- ];
-
-export default function Home() {
- const [selectedComplexities, setSelectedComplexities] = useState
(
- complexityList.map((diff) => diff.value)
- );
- const [selectedCategories, setSelectedCategories] = useState(
- categoryList.map((category) => category.value)
- );
- const [filtersHeight, setFiltersHeight] = useState(0);
- const [questionList, setQuestionList] = useState([]); // Complete list of questions
- const [selectedViewQuestion, setSelectedViewQuestion] =
- useState(null);
- const [isSelectAll, setIsSelectAll] = useState(false);
- const [reset, setReset] = useState(false);
-
- // Fetch questions from backend API
- useEffect(() => {
- async function fetchQuestions() {
- try {
- const response = await fetch(`${process.env.NEXT_PUBLIC_QUESTION_API_BASE_URL}/all`, {
- cache: "no-store",
- });
- const data = await response.json();
-
- // Map backend data to match the frontend Question type
- const mappedQuestions: Question[] = data.map((q: {id: number, title: string, complexity: string, category: string[], description: string, link: string,selected: boolean}) => ({
- id: q.id,
- title: q.title,
- complexity: complexityList.find(
- (complexity) => complexity.value === q.complexity.toLowerCase()
- )?.value,
- categories: q.category.sort((a: string, b: string) => a.localeCompare(b)),
- description: q.description,
- link: q.link,
- selected: false, // Set selected to false initially
- }));
-
- setQuestionList(mappedQuestions); // Set the fetched data to state
- } catch (error) {
- console.error("Error fetching questions:", error);
- }
- }
-
- fetchQuestions();
- }, []);
-
- useEffect(() => {
- const filtersElement = document.getElementById("filters");
- if (filtersElement) {
- setFiltersHeight(filtersElement.offsetHeight);
- }
- }, []);
-
- // Handle filtered questions based on user-selected complexities and categories
- const filteredQuestions = questionList.filter((question) => {
- const selectedcategoryLabels = selectedCategories.map(
- (categoryValue) =>
- categoryList.find((category) => category.value === categoryValue)?.label
- );
-
- const matchesComplexity =
- selectedComplexities.length === 0 ||
- (question.complexity &&
- selectedComplexities.includes(question.complexity));
-
- const matchesCategories =
- selectedCategories.length === 0 ||
- selectedcategoryLabels.some((category) => question.categories.includes(category));
-
- return matchesComplexity && matchesCategories;
- });
-
- // Function to reset filters
- const resetFilters = () => {
- setSelectedComplexities(complexityList.map((diff) => diff.value));
- setSelectedCategories(categoryList.map((category) => category.value));
- setReset(true);
- };
-
- // Function to handle "Select All" button click
- const handleSelectAll = () => {
- const newIsSelectAll = !isSelectAll;
- setIsSelectAll(newIsSelectAll);
-
- // Toggle selection of all questions
- const updatedQuestions = questionList.map((question) =>
- filteredQuestions.map((f_qns) => f_qns.id).includes(question.id)
- ? {
- ...question,
- selected: newIsSelectAll, // Select or unselect all questions
- }
- : question
- );
- setQuestionList(updatedQuestions);
- };
-
- // Function to handle individual question selection
- const handleSelectQuestion = (id: number) => {
- const updatedQuestions = questionList.map((question) =>
- question.id === id
- ? { ...question, selected: !question.selected }
- : question
- );
- setQuestionList(updatedQuestions);
- };
-
- useEffect(() => {
- const allSelected =
- questionList.length > 0 && questionList.every((q) => q.selected);
- const noneSelected =
- questionList.length > 0 && questionList.every((q) => !q.selected);
-
- if (allSelected) {
- setIsSelectAll(true);
- } else if (noneSelected) {
- setIsSelectAll(false);
- }
- }, [questionList]);
-
- useEffect(() => {
- if (filteredQuestions.length === 0) {
- setSelectedViewQuestion(null);
- }
- }, [filteredQuestions]);
-
-
- useEffect(() => {
- console.log("Selected complexities:", selectedComplexities);
- }, [selectedComplexities]); // This effect runs every time selectedcomplexities change
-
- return (
- //
-
-
-
-
-
-
-
-
-
-
-
-
- {filteredQuestions.length > 0 && (
-
- )}
-
-
-
-
-
- {filteredQuestions.length == 0 ? (
-
-
No questions found
-
-
- ) : (
- filteredQuestions.map((question) => (
-
-
setSelectedViewQuestion(question)}
- >
-
-
- {question.title}
-
-
-
- {question.complexity}
-
- {question.categories.map((category, index) => (
-
- {category}
-
- ))}
-
-
-
-
-
- ))
- )}
-
-
-
-
-
- {!selectedViewQuestion ? (
-
Select a question to view
- ) : (
-
-
- {selectedViewQuestion.title}
-
-
-
-
-
- {selectedViewQuestion.complexity}
-
-
-
-
- {selectedViewQuestion.categories.map((category) => (
-
- {category}
-
- ))}
-
-
-
- {selectedViewQuestion.description}
-
-
- )}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/frontend/app/reset-password/page.tsx b/frontend/app/reset-password/page.tsx
new file mode 100644
index 0000000000..c57478510f
--- /dev/null
+++ b/frontend/app/reset-password/page.tsx
@@ -0,0 +1,199 @@
+"use client"
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import Link from "next/link";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { useEffect, useState } from "react";
+import { AlertCircle, LoaderCircle, TriangleAlert } from "lucide-react";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+
+const formSchema = z.object({
+ email: z.string().min(1, "Email is required").email({ message: "Invalid email address" }),
+ password: z
+ .string()
+ .min(8, "Password must be at least 8 characters")
+ .regex(/[A-Z]/, "Password must contain at least one uppercase letter")
+ .regex(/[a-z]/, "Password must contain at least one lowercase letter")
+ .regex(/[0-9]/, "Password must contain at least one number")
+ .regex(/[^A-Za-z0-9]/, "Password must contain at least one special character"),
+ confirm: z.string().min(8, "Passwords do not match"),
+}).refine((data) => data.password === data.confirm, {
+ message: "Passwords do not match",
+ path: ["confirm"],
+});
+
+export default function ResetPassword() {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState
(null);
+ const [success, setSuccess] = useState(false);
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: "", // Prefill from email link/backend
+ password: "",
+ confirm: "",
+ },
+ });
+
+ const watchPassword = form.watch("password");
+ useEffect(() => {
+ if (watchPassword) {
+ form.trigger("password");
+ }
+ }, [watchPassword, form]);
+
+ const watchConfirm = form.watch("confirm");
+ useEffect(() => {
+ if (watchConfirm) {
+ form.trigger("confirm");
+ }
+ }, [watchConfirm, form]);
+
+ async function onSubmit(values: z.infer) {
+ // Placeholder for auth to user service
+ try {
+ await form.trigger();
+ if (!form.formState.isValid) {
+ return;
+ }
+
+ setIsLoading(true);
+
+ const { confirm, ...resetValues } = values;
+ const response = await fetch(`${process.env.NEXT_PUBLIC_USER_API_AUTH_URL}/reset`, {
+ method: "POST",
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(resetValues),
+ });
+
+ if (response.status == 400) {
+ setError("Missing email.");
+ throw new Error("Missing email: " + response.statusText);
+ } else if (response.status == 500) {
+ setError("Database or server error. Please try again.");
+ throw new Error("Database or server error: " + response.statusText);
+ } else if (!response.ok) {
+ setError("There was an error resetting your password. Please try again.");
+ throw new Error("Error resetting password: " + response.statusText);
+ }
+
+ const responseData = await response.json();
+ setSuccess(true);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+ Reset your password
+
+
+ {error && (
+
+
+ Error
+
+ {error}
+
+
+ )}
+ {success && (
+
+
+ Password has been reset
+
+ Login here with your new password.
+
+
+ )}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/app/signup/page.tsx b/frontend/app/signup/page.tsx
index e863dc4f64..2334c98a4e 100644
--- a/frontend/app/signup/page.tsx
+++ b/frontend/app/signup/page.tsx
@@ -11,14 +11,14 @@ import { useForm } from "react-hook-form"
import {
Form,
FormControl,
+ FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
-import { AlertCircle, Info, LoaderCircle } from "lucide-react"
+import { AlertCircle, LoaderCircle } from "lucide-react"
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
const formSchema = z.object({
username: z.string().min(4, "Username requires at least 4 characters"),
@@ -134,10 +134,10 @@ export default function Signup() {
return (
-
-
PeerPrep
+
+ PeerPrep
-
+
Create an account
@@ -163,23 +163,12 @@ export default function Signup() {
name="username"
render={({ field }) => (
-
-
- Username
-
-
-
-
- Minimum 4 characters
-
-
-
-
-
+ Username
+ Minimum 4 characters
)}
/>
@@ -201,30 +190,21 @@ export default function Signup() {
name="password"
render={({ field }) => (
-
-
- Password
-
-
-
-
- Password must have at least:
-
- - 8 characters
- - 1 uppercase character
- - 1 lowercase character
- - 1 number
- - 1 special character
-
-
-
-
-
-
+ Password
+
+ Must have at least:
+
+ - 8 characters
+ - 1 uppercase character
+ - 1 lowercase character
+ - 1 number
+ - 1 special character
+
+
)}
/>
@@ -258,7 +238,7 @@ export default function Signup() {
Already have an account?{" "}
Sign in
diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx
index 0ba4277355..3d357d3016 100644
--- a/frontend/components/ui/button.tsx
+++ b/frontend/components/ui/button.tsx
@@ -13,7 +13,7 @@ const buttonVariants = cva(
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
- "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ "border border-gray-200 bg-background hover:bg-brand-100 hover:text-brand-700",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
diff --git a/frontend/components/ui/form.tsx b/frontend/components/ui/form.tsx
index 20274fb1c6..23838d70b2 100644
--- a/frontend/components/ui/form.tsx
+++ b/frontend/components/ui/form.tsx
@@ -132,7 +132,7 @@ const FormDescription = React.forwardRef<
const { formDescriptionId } = useFormField()
return (
-