From dc85ba0f08e155f102779fbd913bea2b24afbb03 Mon Sep 17 00:00:00 2001 From: Brendan Tan Date: Tue, 15 Oct 2024 16:40:39 +0800 Subject: [PATCH 1/4] Add basic profile page --- frontend/app/layout.tsx | 6 +- frontend/app/profile/page.tsx | 145 ++++++++++++++++++++++++++++ frontend/app/question-repo/page.tsx | 34 ++++--- frontend/app/questions/page.tsx | 35 ++++--- frontend/app/signup/page.tsx | 2 +- frontend/components/ui/button.tsx | 2 +- frontend/tailwind.config.ts | 2 +- 7 files changed, 197 insertions(+), 29 deletions(-) create mode 100644 frontend/app/profile/page.tsx diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 1308ca9870..6fcee12e33 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -3,11 +3,11 @@ import { Syne } from 'next/font/google' import localFont from "next/font/local"; import "./globals.css"; -const brandingFont = Syne({ +const brandFont = Syne({ subsets: ['latin'], weight: ["400", "500", "600", "700", "800"], display: "swap", - variable: "--font-branding", + variable: "--font-brand", }); const matterFont = localFont({ src: "./fonts/MatterTRIALVF-Uprights.woff2", @@ -35,7 +35,7 @@ export default function RootLayout({ return ( {children} diff --git a/frontend/app/profile/page.tsx b/frontend/app/profile/page.tsx new file mode 100644 index 0000000000..7e32279c13 --- /dev/null +++ b/frontend/app/profile/page.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { CircleX, LogOut, Pencil, Save, User } from "lucide-react"; +import Link from "next/link"; +import React, { ChangeEvent, useState } from "react"; + +export default function Home() { + const [isEditing, setIsEditing] = useState(false); + const [userData, setUserData] = useState({ + username: "johndoe", + email: "john@example.com", + password: "abcdefgh", + }); + + const handleEdit = () => { + if (isEditing) { + console.log("Saving changes:", userData); + } + setIsEditing(!isEditing); + }; + + const handleInputChange = (e: ChangeEvent) => { + const { id, value } = e.target; + setUserData(prev => ({ ...prev, [id]: value })); + }; + + return ( +
+
+
+ + PeerPrep + + {process.env.NODE_ENV == "development" && ( + + DEV + + )} +
+
+ +
+
+ +
+ + + Profile + {isEditing ? ( +
+ + +
+ ) : ( + + )} +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +

42

+
+
+ +

38

+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/question-repo/page.tsx b/frontend/app/question-repo/page.tsx index 4dfbdce01e..6867b14d8b 100644 --- a/frontend/app/question-repo/page.tsx +++ b/frontend/app/question-repo/page.tsx @@ -6,7 +6,8 @@ import { DataTable } from "./data-table" import { Badge, BadgeProps } from "@/components/ui/badge"; import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { User, LogOut } from "lucide-react"; const complexityList: Array<{ value: string; @@ -82,31 +83,42 @@ export default function QuestionRepo() {
PeerPrep {process.env.NODE_ENV == "development" && ( - + DEV )}
-
diff --git a/frontend/app/questions/page.tsx b/frontend/app/questions/page.tsx index 3152b62757..652441da9e 100644 --- a/frontend/app/questions/page.tsx +++ b/frontend/app/questions/page.tsx @@ -4,9 +4,10 @@ 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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { MultiSelect } from "@/components/ui/multi-select"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Flag, MessageSquareText } from "lucide-react"; +import { Flag, LogOut, MessageSquareText, User } from "lucide-react"; import Link from "next/link"; import React, { useEffect, useState } from "react"; @@ -178,37 +179,47 @@ export default function Home() { }, [selectedComplexities]); // This effect runs every time selectedcomplexities change return ( - //
PeerPrep {process.env.NODE_ENV == "development" && ( - + DEV )}
-
diff --git a/frontend/app/signup/page.tsx b/frontend/app/signup/page.tsx index e863dc4f64..6842c473c1 100644 --- a/frontend/app/signup/page.tsx +++ b/frontend/app/signup/page.tsx @@ -135,7 +135,7 @@ export default function Signup() { return (
- PeerPrep + PeerPrep
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/tailwind.config.ts b/frontend/tailwind.config.ts index 00e9aecd08..0b71205aeb 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -87,7 +87,7 @@ const config: Config = { sm: 'calc(var(--radius) - 4px)' }, fontFamily: { - branding: ['var(--font-branding)'], + brand: ['var(--font-brand)'], sans: ['var(--font-matter)'], serif: ['var(--font-reckless-neue)'], inter: ['var(--font-inter)'] From 914979fa79e0509f8481f0267d5f12e1ad09e20f Mon Sep 17 00:00:00 2001 From: Brendan Tan Date: Tue, 15 Oct 2024 18:15:06 +0800 Subject: [PATCH 2/4] Refactor pages for organization purposes --- frontend/app/(authenticated)/layout.tsx | 80 ++++ frontend/app/(authenticated)/profile/page.tsx | 96 +++++ .../add-edit-question-dialog.tsx | 0 .../question-repo/columns.tsx | 0 .../data-table-column-header.tsx | 0 .../data-table-faceted-filter.tsx | 0 .../question-repo/data-table-pagination.tsx | 2 +- .../question-repo/data-table-row-actions.tsx | 0 .../question-repo/data-table-toolbar.tsx | 0 .../question-repo/data-table.tsx | 11 +- .../question-repo/data.tsx | 0 .../question-repo/del-question-dialog.tsx | 0 .../(authenticated)/question-repo/page.tsx | 82 ++++ .../app/(authenticated)/questions/page.tsx | 320 +++++++++++++++ frontend/app/profile/page.tsx | 145 ------- frontend/app/question-repo/page.tsx | 132 ------- frontend/app/questions/page.tsx | 371 ------------------ frontend/tailwind.config.ts | 4 + 18 files changed, 591 insertions(+), 652 deletions(-) create mode 100644 frontend/app/(authenticated)/layout.tsx create mode 100644 frontend/app/(authenticated)/profile/page.tsx rename frontend/app/{ => (authenticated)}/question-repo/add-edit-question-dialog.tsx (100%) rename frontend/app/{ => (authenticated)}/question-repo/columns.tsx (100%) rename frontend/app/{ => (authenticated)}/question-repo/data-table-column-header.tsx (100%) rename frontend/app/{ => (authenticated)}/question-repo/data-table-faceted-filter.tsx (100%) rename frontend/app/{ => (authenticated)}/question-repo/data-table-pagination.tsx (98%) rename frontend/app/{ => (authenticated)}/question-repo/data-table-row-actions.tsx (100%) rename frontend/app/{ => (authenticated)}/question-repo/data-table-toolbar.tsx (100%) rename frontend/app/{ => (authenticated)}/question-repo/data-table.tsx (93%) rename frontend/app/{ => (authenticated)}/question-repo/data.tsx (100%) rename frontend/app/{ => (authenticated)}/question-repo/del-question-dialog.tsx (100%) create mode 100644 frontend/app/(authenticated)/question-repo/page.tsx create mode 100644 frontend/app/(authenticated)/questions/page.tsx delete mode 100644 frontend/app/profile/page.tsx delete mode 100644 frontend/app/question-repo/page.tsx delete mode 100644 frontend/app/questions/page.tsx diff --git a/frontend/app/(authenticated)/layout.tsx b/frontend/app/(authenticated)/layout.tsx new file mode 100644 index 0000000000..93f937d34d --- /dev/null +++ b/frontend/app/(authenticated)/layout.tsx @@ -0,0 +1,80 @@ +"use client" + +import Link from "next/link"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { User, LogOut } from "lucide-react"; +import { usePathname } from "next/navigation"; + +export default function AuthenticatedLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const pathname = usePathname(); + + return ( +
+
+
+ + PeerPrep + + {process.env.NODE_ENV == "development" && ( + + DEV + + )} +
+
+ +
+
+ {children} +
+ ); +} diff --git a/frontend/app/(authenticated)/profile/page.tsx b/frontend/app/(authenticated)/profile/page.tsx new file mode 100644 index 0000000000..c00d27dd48 --- /dev/null +++ b/frontend/app/(authenticated)/profile/page.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { CircleX, Pencil, Save } from "lucide-react"; +import React, { ChangeEvent, useState } from "react"; + +export default function Home() { + const [isEditing, setIsEditing] = useState(false); + const [userData, setUserData] = useState({ + username: "johndoe", + email: "john@example.com", + password: "abcdefgh", + }); + + const handleEdit = () => { + if (isEditing) { + console.log("Saving changes:", userData); + } + setIsEditing(!isEditing); + }; + + const handleInputChange = (e: ChangeEvent) => { + const { id, value } = e.target; + setUserData(prev => ({ ...prev, [id]: value })); + }; + + return ( +
+ + + Profile + {isEditing ? ( +
+ + +
+ ) : ( + + )} +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +

42

+
+
+ +

38

+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/question-repo/add-edit-question-dialog.tsx b/frontend/app/(authenticated)/question-repo/add-edit-question-dialog.tsx similarity index 100% rename from frontend/app/question-repo/add-edit-question-dialog.tsx rename to frontend/app/(authenticated)/question-repo/add-edit-question-dialog.tsx diff --git a/frontend/app/question-repo/columns.tsx b/frontend/app/(authenticated)/question-repo/columns.tsx similarity index 100% rename from frontend/app/question-repo/columns.tsx rename to frontend/app/(authenticated)/question-repo/columns.tsx diff --git a/frontend/app/question-repo/data-table-column-header.tsx b/frontend/app/(authenticated)/question-repo/data-table-column-header.tsx similarity index 100% rename from frontend/app/question-repo/data-table-column-header.tsx rename to frontend/app/(authenticated)/question-repo/data-table-column-header.tsx diff --git a/frontend/app/question-repo/data-table-faceted-filter.tsx b/frontend/app/(authenticated)/question-repo/data-table-faceted-filter.tsx similarity index 100% rename from frontend/app/question-repo/data-table-faceted-filter.tsx rename to frontend/app/(authenticated)/question-repo/data-table-faceted-filter.tsx diff --git a/frontend/app/question-repo/data-table-pagination.tsx b/frontend/app/(authenticated)/question-repo/data-table-pagination.tsx similarity index 98% rename from frontend/app/question-repo/data-table-pagination.tsx rename to frontend/app/(authenticated)/question-repo/data-table-pagination.tsx index 403e11e43f..1a69a34c3e 100644 --- a/frontend/app/question-repo/data-table-pagination.tsx +++ b/frontend/app/(authenticated)/question-repo/data-table-pagination.tsx @@ -41,7 +41,7 @@ export function DataTablePagination({ - {[10, 20, 30, 40, 50].map((pageSize) => ( + {[5, 10, 20, 30, 40, 50].map((pageSize) => ( {pageSize} diff --git a/frontend/app/question-repo/data-table-row-actions.tsx b/frontend/app/(authenticated)/question-repo/data-table-row-actions.tsx similarity index 100% rename from frontend/app/question-repo/data-table-row-actions.tsx rename to frontend/app/(authenticated)/question-repo/data-table-row-actions.tsx diff --git a/frontend/app/question-repo/data-table-toolbar.tsx b/frontend/app/(authenticated)/question-repo/data-table-toolbar.tsx similarity index 100% rename from frontend/app/question-repo/data-table-toolbar.tsx rename to frontend/app/(authenticated)/question-repo/data-table-toolbar.tsx diff --git a/frontend/app/question-repo/data-table.tsx b/frontend/app/(authenticated)/question-repo/data-table.tsx similarity index 93% rename from frontend/app/question-repo/data-table.tsx rename to frontend/app/(authenticated)/question-repo/data-table.tsx index 68306ba3cc..ce9cbb208e 100644 --- a/frontend/app/question-repo/data-table.tsx +++ b/frontend/app/(authenticated)/question-repo/data-table.tsx @@ -82,6 +82,11 @@ export function DataTable({ columnFilters, rowSelection }, + initialState: { + pagination: { + pageSize: 5, + }, + }, meta: { removeSelectedRows: (selectedRows: number[]) => { const filterFunc = (old: TData[]) => { @@ -99,12 +104,12 @@ export function DataTable({ }) return ( -
+
-
+
{table.getHeaderGroups().map((headerGroup) => ( @@ -124,7 +129,7 @@ export function DataTable({ ))} - + {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( = [ + { 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 QuestionRepo() { + const [questionList, setQuestionList] = useState([]); // Complete list of questions + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchQuestions() { + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_QUESTION_API_BASE_URL}/all`, { + cache: "no-store", + }); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + 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[], summary: 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)), + summary: q.summary, + description: q.description, + link: q.link, + selected: false, // Set selected to false initially + })); + console.log("question list: ", mappedQuestions) + setQuestionList(mappedQuestions); // Set the fetched data to state + setLoading(false); + } catch (error) { + console.error("Error fetching questions from server:", error); + } + } + + fetchQuestions(); + }, []); + + return ( +
+
Question Repository
+ +
+ ); +} diff --git a/frontend/app/(authenticated)/questions/page.tsx b/frontend/app/(authenticated)/questions/page.tsx new file mode 100644 index 0000000000..46431040fd --- /dev/null +++ b/frontend/app/(authenticated)/questions/page.tsx @@ -0,0 +1,320 @@ +"use client"; + +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 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) { + const filtersRect = filtersElement.getBoundingClientRect(); + const totalHeight = filtersRect.bottom; + setFiltersHeight(totalHeight+16); + } + }, []); + + // 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/profile/page.tsx b/frontend/app/profile/page.tsx deleted file mode 100644 index 7e32279c13..0000000000 --- a/frontend/app/profile/page.tsx +++ /dev/null @@ -1,145 +0,0 @@ -"use client"; - -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { CircleX, LogOut, Pencil, Save, User } from "lucide-react"; -import Link from "next/link"; -import React, { ChangeEvent, useState } from "react"; - -export default function Home() { - const [isEditing, setIsEditing] = useState(false); - const [userData, setUserData] = useState({ - username: "johndoe", - email: "john@example.com", - password: "abcdefgh", - }); - - const handleEdit = () => { - if (isEditing) { - console.log("Saving changes:", userData); - } - setIsEditing(!isEditing); - }; - - const handleInputChange = (e: ChangeEvent) => { - const { id, value } = e.target; - setUserData(prev => ({ ...prev, [id]: value })); - }; - - return ( -
-
-
- - PeerPrep - - {process.env.NODE_ENV == "development" && ( - - DEV - - )} -
-
- -
-
- -
- - - Profile - {isEditing ? ( -
- - -
- ) : ( - - )} -
- -
-
- - -
-
- - -
-
- - -
-
-
- -

42

-
-
- -

38

-
-
-
-
-
-
-
- ); -} \ No newline at end of file diff --git a/frontend/app/question-repo/page.tsx b/frontend/app/question-repo/page.tsx deleted file mode 100644 index 6867b14d8b..0000000000 --- a/frontend/app/question-repo/page.tsx +++ /dev/null @@ -1,132 +0,0 @@ -"use client" - -import { useEffect, useState } from "react"; -import { columns, Question } from "./columns" -import { DataTable } from "./data-table" -import { Badge, BadgeProps } from "@/components/ui/badge"; -import Link from "next/link"; -import { Button } from "@/components/ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { User, LogOut } from "lucide-react"; - -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 QuestionRepo() { - const [questionList, setQuestionList] = useState([]); // Complete list of questions - const [loading, setLoading] = useState(true); - - useEffect(() => { - async function fetchQuestions() { - try { - const response = await fetch(`${process.env.NEXT_PUBLIC_QUESTION_API_BASE_URL}/all`, { - cache: "no-store", - }); - if (!response.ok) { - throw new Error("Network response was not ok"); - } - 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[], summary: 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)), - summary: q.summary, - description: q.description, - link: q.link, - selected: false, // Set selected to false initially - })); - console.log("question list: ", mappedQuestions) - setQuestionList(mappedQuestions); // Set the fetched data to state - setLoading(false); - } catch (error) { - console.error("Error fetching questions from server:", error); - } - } - - fetchQuestions(); - }, []); - - return ( -
-
-
- - PeerPrep - - {process.env.NODE_ENV == "development" && ( - - DEV - - )} -
-
- -
-
- -
-
Question Repository
- -
-
- ); -} diff --git a/frontend/app/questions/page.tsx b/frontend/app/questions/page.tsx deleted file mode 100644 index 652441da9e..0000000000 --- a/frontend/app/questions/page.tsx +++ /dev/null @@ -1,371 +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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { MultiSelect } from "@/components/ui/multi-select"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Flag, LogOut, MessageSquareText, User } 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 ( -
-
-
- - PeerPrep - - {process.env.NODE_ENV == "development" && ( - - DEV - - )} -
-
- -
-
- -
-
-
-
- -
- - -
- {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/tailwind.config.ts b/frontend/tailwind.config.ts index 0b71205aeb..444e183cee 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -96,6 +96,10 @@ const config: Config = { 'tablet': '640px', 'laptop': '1024px', 'desktop': '1280px', + }, + dropShadow: { + 'question-card': '0px 2px 4px rgba(0, 0, 0, 0.15)', + 'question-details': '0px 8px 8px rgba(0, 0, 0, 0.15)' } }, }, From 1700ebd01adb3e15687f549bdebb0720808bd19b Mon Sep 17 00:00:00 2001 From: Brendan Tan Date: Tue, 15 Oct 2024 22:18:08 +0800 Subject: [PATCH 3/4] Add forgot and reset password --- frontend/app/(authenticated)/profile/page.tsx | 20 +- frontend/app/forgot-password/page.tsx | 163 ++++++++++++++ frontend/app/page.tsx | 11 +- frontend/app/reset-password/page.tsx | 200 ++++++++++++++++++ frontend/app/signup/page.tsx | 54 ++--- frontend/components/ui/form.tsx | 2 +- 6 files changed, 402 insertions(+), 48 deletions(-) create mode 100644 frontend/app/forgot-password/page.tsx create mode 100644 frontend/app/reset-password/page.tsx diff --git a/frontend/app/(authenticated)/profile/page.tsx b/frontend/app/(authenticated)/profile/page.tsx index c00d27dd48..37acac9d3b 100644 --- a/frontend/app/(authenticated)/profile/page.tsx +++ b/frontend/app/(authenticated)/profile/page.tsx @@ -28,8 +28,8 @@ export default function Home() { }; return ( -
- +
+ Profile {isEditing ? ( @@ -42,7 +42,7 @@ export default function Home() { ) : ( - )} @@ -79,13 +79,17 @@ export default function Home() { />
-
+
-

42

+
+ 11 + / + 20 +
-
- -

38

+
+ +

14

diff --git a/frontend/app/forgot-password/page.tsx b/frontend/app/forgot-password/page.tsx new file mode 100644 index 0000000000..06ad77cd39 --- /dev/null +++ b/frontend/app/forgot-password/page.tsx @@ -0,0 +1,163 @@ +"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, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { useEffect, useState } from "react"; +import { AlertCircle, ChevronLeft, 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" }), +}) + +export default function ForgotPassword() { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const [countdown, setCountdown] = useState(60); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + }, + }); + + useEffect(() => { + let timer: NodeJS.Timeout; + if (success && countdown > 0) { + timer = setInterval(() => { + setCountdown((prevCount) => prevCount - 1); + }, 1000); + } + return () => { + if (timer) clearInterval(timer); + }; + }, [success, countdown]); + + useEffect(() => { + if (countdown === 0) { + setSuccess(false); + setCountdown(60); + } + }, [countdown]); + + async function onSubmit(values: z.infer) { + // Placeholder for auth to user service + try { + await form.trigger(); + if (!form.formState.isValid) { + return; + } + + setIsLoading(true); + + const response = await fetch(`${process.env.NEXT_PUBLIC_USER_API_AUTH_URL}/forgot`, { + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(values), + }); + + 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 sending reset link: " + response.statusText); + } + + const responseData = await response.json(); + setSuccess(true); + setCountdown(60); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + } + + return ( +
+
+
+ +
+
+ + Forgot your password? + +

+ Enter your email address and we will send you a link to reset your password. +

+
+ {error && ( + + + Error + + {error} + + + )} + {success && ( + + + Check your email + + A link to reset your password has been sent to your email address. + + + )} +
+
+ + ( + + Email + + + + + + )} + /> + + + +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 873a3299af..110000cb96 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -80,7 +80,7 @@ export default function Login() { } return ( -
+
@@ -117,7 +117,14 @@ export default function Login() { name="password" render={({ field }) => ( - Password + +
+ Password + + Forgot password? + +
+
diff --git a/frontend/app/reset-password/page.tsx b/frontend/app/reset-password/page.tsx new file mode 100644 index 0000000000..614e81daa6 --- /dev/null +++ b/frontend/app/reset-password/page.tsx @@ -0,0 +1,200 @@ +"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 { useRouter } from "next/navigation" +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. + + + )} +
+
+ + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + Password must have at least: +
    +
  • 8 characters
  • +
  • 1 uppercase character
  • +
  • 1 lowercase character
  • +
  • 1 number
  • +
  • 1 special character
  • +
+
+
+ )} + /> + ( + + Confirm password + + + + + + )} + /> + + + +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/app/signup/page.tsx b/frontend/app/signup/page.tsx index 6842c473c1..228e3728d3 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
-
+
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
  • +
+
)} /> 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 ( -

Date: Tue, 15 Oct 2024 22:42:49 +0800 Subject: [PATCH 4/4] Add temporary landing page --- frontend/app/(authenticated)/layout.tsx | 2 +- frontend/app/forgot-password/page.tsx | 2 +- frontend/app/login/page.tsx | 161 ++++++++++++++++++++++ frontend/app/page.tsx | 173 +++--------------------- frontend/app/reset-password/page.tsx | 3 +- frontend/app/signup/page.tsx | 2 +- 6 files changed, 185 insertions(+), 158 deletions(-) create mode 100644 frontend/app/login/page.tsx diff --git a/frontend/app/(authenticated)/layout.tsx b/frontend/app/(authenticated)/layout.tsx index 93f937d34d..7ab2f1e87c 100644 --- a/frontend/app/(authenticated)/layout.tsx +++ b/frontend/app/(authenticated)/layout.tsx @@ -68,7 +68,7 @@ export default function AuthenticatedLayout({ Username Profile - Log out + Log out diff --git a/frontend/app/forgot-password/page.tsx b/frontend/app/forgot-password/page.tsx index 06ad77cd39..d23086fba2 100644 --- a/frontend/app/forgot-password/page.tsx +++ b/frontend/app/forgot-password/page.tsx @@ -97,7 +97,7 @@ export default function ForgotPassword() {

- +
diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx new file mode 100644 index 0000000000..110000cb96 --- /dev/null +++ b/frontend/app/login/page.tsx @@ -0,0 +1,161 @@ +"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, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { useRouter } from "next/navigation" +import { useState } from "react"; +import { AlertCircle, LoaderCircle } 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 requires at least 8 characters"), // Password has more criterias but we only let user know about length +}) + +export default function Login() { + const router = useRouter() + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + password: "", + }, + }); + + async function onSubmit(values: z.infer) { + // Placeholder for auth to user service + try { + await form.trigger(); + if (!form.formState.isValid) { + return; + } + + setIsLoading(true); + + const response = await fetch(`${process.env.NEXT_PUBLIC_USER_API_AUTH_URL}/login`, { + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(values), + }); + + if (response.status == 400) { + setError("Missing email or password."); + throw new Error("Missing email or password: " + response.statusText); + } else if (response.status == 401) { + setError("Incorrect email or password."); + throw new Error("Incorrect email or password: " + 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 logging in. Please try again."); + throw new Error("Error logging in: " + response.statusText); + } + + const responseData = await response.json(); + console.log(responseData.data["accessToken"]); + router.push("/question-repo"); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + } + + return ( +
+
+
+ + Sign in + +
+ {error && ( + + + Error + + {error} + + + )} +
+
+ + ( + + Email + + + + + + )} + /> + ( + + +
+ Password + + Forgot password? + +
+
+ + + + +
+ )} + /> + + + +
+ Don't have an account?{" "} + + Sign up + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 110000cb96..e5262b0635 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,160 +1,27 @@ -"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, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { useRouter } from "next/navigation" -import { useState } from "react"; -import { AlertCircle, LoaderCircle } 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 requires at least 8 characters"), // Password has more criterias but we only let user know about length -}) - -export default function Login() { - const router = useRouter() - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - email: "", - password: "", - }, - }); - - async function onSubmit(values: z.infer) { - // Placeholder for auth to user service - try { - await form.trigger(); - if (!form.formState.isValid) { - return; - } - - setIsLoading(true); - - const response = await fetch(`${process.env.NEXT_PUBLIC_USER_API_AUTH_URL}/login`, { - method: "POST", - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(values), - }); - - if (response.status == 400) { - setError("Missing email or password."); - throw new Error("Missing email or password: " + response.statusText); - } else if (response.status == 401) { - setError("Incorrect email or password."); - throw new Error("Incorrect email or password: " + 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 logging in. Please try again."); - throw new Error("Error logging in: " + response.statusText); - } - - const responseData = await response.json(); - console.log(responseData.data["accessToken"]); - router.push("/question-repo"); - } catch (error) { - console.error(error); - } finally { - setIsLoading(false); - } - } +export default function Landing() { return ( -
-
-
- - Sign in - -
- {error && ( - - - Error - - {error} - - - )} -
-
- - ( - - Email - - - - - - )} - /> - ( - - -
- Password - - Forgot password? - -
-
- - - - -
- )} - /> - - - -
- 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/reset-password/page.tsx b/frontend/app/reset-password/page.tsx index 614e81daa6..c57478510f 100644 --- a/frontend/app/reset-password/page.tsx +++ b/frontend/app/reset-password/page.tsx @@ -15,7 +15,6 @@ import { FormLabel, FormMessage, } from "@/components/ui/form" -import { useRouter } from "next/navigation" import { useEffect, useState } from "react"; import { AlertCircle, LoaderCircle, TriangleAlert } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; @@ -124,7 +123,7 @@ export default function ResetPassword() { Password has been reset - Login here with your new password. + Login here with your new password. )} diff --git a/frontend/app/signup/page.tsx b/frontend/app/signup/page.tsx index 228e3728d3..2334c98a4e 100644 --- a/frontend/app/signup/page.tsx +++ b/frontend/app/signup/page.tsx @@ -238,7 +238,7 @@ export default function Signup() {
Already have an account?{" "} Sign in