diff --git a/frontend/app/app/matching/page.tsx b/frontend/app/app/matching/page.tsx new file mode 100644 index 0000000000..f1e16a7a60 --- /dev/null +++ b/frontend/app/app/matching/page.tsx @@ -0,0 +1,13 @@ +import AuthPageWrapper from "@/components/auth/auth-page-wrapper"; +import FindMatch from "@/components/matching/find-match"; +import { Suspense } from "react"; + +export default function MatchingPage() { + return ( + + + + + + ); +} diff --git a/frontend/app/app/user-settings/[user_id]/page.tsx b/frontend/app/app/user-settings/[user_id]/page.tsx index d11c25ed1a..3cc6a9b1b5 100644 --- a/frontend/app/app/user-settings/[user_id]/page.tsx +++ b/frontend/app/app/user-settings/[user_id]/page.tsx @@ -8,7 +8,7 @@ export default function UserSettingsPage({ }) { return ( - ; + ); } diff --git a/frontend/components/admin-user-management/admin-user-management.tsx b/frontend/components/admin-user-management/admin-user-management.tsx index d4b7679074..06f45b688d 100644 --- a/frontend/components/admin-user-management/admin-user-management.tsx +++ b/frontend/components/admin-user-management/admin-user-management.tsx @@ -14,6 +14,7 @@ import { } from "@/components/ui/table"; import LoadingScreen from "@/components/common/loading-screen"; import AdminEditUserModal from "@/components/admin-user-management/admin-edit-user-modal"; +import DeleteAccountModal from "@/components/common/delete-account-modal"; import { PencilIcon, Trash2Icon } from "lucide-react"; import { User, UserArraySchema } from "@/lib/schemas/user-schema"; import { userServiceUri } from "@/lib/api/api-uri"; @@ -51,6 +52,9 @@ export default function AdminUserManagement() { const [users, setUsers] = useState([]); const [showModal, setShowModal] = useState(false); const [selectedUser, setSelectedUser] = useState(); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [confirmUsername, setConfirmUsername] = useState(""); + const [isDeleteButtonEnabled, setIsDeleteButtonEnabled] = useState(false); useEffect(() => { if (data) { @@ -58,18 +62,23 @@ export default function AdminUserManagement() { } }, [data]); + // Enable delete button in the delete account modal only when the input username matches the original username + useEffect(() => { + setIsDeleteButtonEnabled(confirmUsername === selectedUser?.username); + }, [confirmUsername, selectedUser]); + if (isLoading) { return ; } - const handleDelete = async (userId: string) => { + const handleDelete = async () => { const token = auth?.token; if (!token) { throw new Error("No authentication token found"); } const response = await fetch( - `${userServiceUri(window.location.hostname)}/users/${userId}`, + `${userServiceUri(window.location.hostname)}/users/${selectedUser?.id}`, { method: "DELETE", headers: { @@ -82,7 +91,7 @@ export default function AdminUserManagement() { throw new Error("Failed to delete user"); } - setUsers(users.filter((user) => user.id !== userId)); + setUsers(users.filter((user) => user.id !== selectedUser?.id)); }; const onUserUpdate = () => { @@ -100,6 +109,16 @@ export default function AdminUserManagement() { user={selectedUser} onUserUpdate={onUserUpdate} /> + @@ -130,7 +149,10 @@ export default function AdminUserManagement() { diff --git a/frontend/components/user-settings/delete-account-modal.tsx b/frontend/components/common/delete-account-modal.tsx similarity index 76% rename from frontend/components/user-settings/delete-account-modal.tsx rename to frontend/components/common/delete-account-modal.tsx index ea10b9b5c8..d8e5b5c230 100644 --- a/frontend/components/user-settings/delete-account-modal.tsx +++ b/frontend/components/common/delete-account-modal.tsx @@ -17,6 +17,7 @@ interface DeleteAccountModalProps { handleDeleteAccount: () => void; isDeleteButtonEnabled: boolean; setShowDeleteModal: (show: boolean) => void; + isAdmin: boolean; } const DeleteAccountModal: React.FC = ({ @@ -27,6 +28,7 @@ const DeleteAccountModal: React.FC = ({ handleDeleteAccount, isDeleteButtonEnabled, setShowDeleteModal, + isAdmin, }) => { return ( <> @@ -37,10 +39,18 @@ const DeleteAccountModal: React.FC = ({ Confirm Delete Account
-

To confirm, please type your username ({originalUsername}):

+ {isAdmin ? ( +

+ To delete, please confirm the username ({originalUsername}): +

+ ) : ( +

+ To confirm, please type your username ({originalUsername}): +

+ )} setConfirmUsername(e.target.value)} /> @@ -57,7 +67,11 @@ const DeleteAccountModal: React.FC = ({ + ) : ( + + )} + + + ); +} diff --git a/frontend/components/matching/search-progress.tsx b/frontend/components/matching/search-progress.tsx new file mode 100644 index 0000000000..ef3d1d0a55 --- /dev/null +++ b/frontend/components/matching/search-progress.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Clock } from "lucide-react"; + +interface SearchProgressProps { + waitTime: number; +} + +export function SearchProgress({ waitTime }: SearchProgressProps) { + return ( + + + Searching for Match + + Please wait while we find a suitable match for you. + + + +
+ +
+
+ + + Wait Time: {Math.floor(waitTime / 60)}: + {(waitTime % 60).toString().padStart(2, "0")} + +
+ Searching... +
+
+
+
+ ); +} diff --git a/frontend/components/matching/selection-summary.tsx b/frontend/components/matching/selection-summary.tsx new file mode 100644 index 0000000000..ce14269d6c --- /dev/null +++ b/frontend/components/matching/selection-summary.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { AlertCircle } from "lucide-react"; + +interface SelectionSummaryProps { + selectedDifficulty: string; + selectedTopic: string; +} + +export function SelectionSummary({ + selectedDifficulty, + selectedTopic, +}: SelectionSummaryProps) { + return ( + + + Selection Summary + +

Difficulty: {selectedDifficulty || "None selected"}

+

Topic: {selectedTopic || "None selected"}

+
+
+ ); +} diff --git a/frontend/components/questions/question-form-modal.tsx b/frontend/components/questions/question-form-modal.tsx index 818a192bb0..94a7fb2828 100644 --- a/frontend/components/questions/question-form-modal.tsx +++ b/frontend/components/questions/question-form-modal.tsx @@ -59,7 +59,11 @@ const QuestionFormModal: React.FC = ({ ...props }) => { e.preventDefault(); props.handleSubmit(question); - setQuestion(initialQuestionState); + if (props.initialData) { + setQuestion(props.initialData); + } else { + setQuestion(initialQuestionState); + } }; const handleExit = () => { diff --git a/frontend/components/ui/progress.tsx b/frontend/components/ui/progress.tsx new file mode 100644 index 0000000000..6f71acfa94 --- /dev/null +++ b/frontend/components/ui/progress.tsx @@ -0,0 +1,36 @@ +"use client"; + +import * as React from "react"; +import * as ProgressPrimitive from "@radix-ui/react-progress"; + +import { cn } from "@/lib/utils"; + +interface ProgressProps + extends React.ComponentPropsWithoutRef { + indeterminate?: boolean; +} + +const Progress = React.forwardRef< + React.ElementRef, + ProgressProps +>(({ className, value, indeterminate = false, ...props }, ref) => ( + + + +)); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; diff --git a/frontend/components/user-settings/user-settings.tsx b/frontend/components/user-settings/user-settings.tsx index 384ee3a121..510ef54a0b 100644 --- a/frontend/components/user-settings/user-settings.tsx +++ b/frontend/components/user-settings/user-settings.tsx @@ -18,7 +18,7 @@ import { import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AlertCircle } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import DeleteAccountModal from "@/components/user-settings/delete-account-modal"; +import DeleteAccountModal from "@/components/common/delete-account-modal"; import ProfileTab from "@/components/user-settings/profile-tab"; import LoadingScreen from "@/components/common/loading-screen"; import { useAuth } from "@/app/auth/auth-context"; @@ -461,6 +461,7 @@ export default function UserSettings({ userId }: { userId: string }) { handleDeleteAccount={handleDeleteAccount} isDeleteButtonEnabled={isDeleteButtonEnabled} setShowDeleteModal={setShowDeleteModal} + isAdmin={false} /> diff --git a/frontend/hooks/use-toast.ts b/frontend/hooks/use-toast.ts deleted file mode 100644 index fc2c6dce91..0000000000 --- a/frontend/hooks/use-toast.ts +++ /dev/null @@ -1,191 +0,0 @@ -"use client"; - -// Inspired by react-hot-toast library -import * as React from "react"; - -import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; - -const TOAST_LIMIT = 1; -const TOAST_REMOVE_DELAY = 1000000; - -type ToasterToast = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const; - -let count = 0; - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER; - return count.toString(); -} - -type ActionType = typeof actionTypes; - -type Action = - | { - type: ActionType["ADD_TOAST"]; - toast: ToasterToast; - } - | { - type: ActionType["UPDATE_TOAST"]; - toast: Partial; - } - | { - type: ActionType["DISMISS_TOAST"]; - toastId?: ToasterToast["id"]; - } - | { - type: ActionType["REMOVE_TOAST"]; - toastId?: ToasterToast["id"]; - }; - -interface State { - toasts: ToasterToast[]; -} - -const toastTimeouts = new Map>(); - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return; - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }); - }, TOAST_REMOVE_DELAY); - - toastTimeouts.set(toastId, timeout); -}; - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - }; - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - }; - - case "DISMISS_TOAST": { - const { toastId } = action; - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId); - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id); - }); - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - }; - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - }; - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - }; - } -}; - -const listeners: Array<(state: State) => void> = []; - -let memoryState: State = { toasts: [] }; - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action); - listeners.forEach((listener) => { - listener(memoryState); - }); -} - -type Toast = Omit; - -function toast({ ...props }: Toast) { - const id = genId(); - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }); - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss(); - }, - }, - }); - - return { - id: id, - dismiss, - update, - }; -} - -function useToast() { - const [state, setState] = React.useState(memoryState); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, [state]); - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - }; -} - -export { useToast, toast }; diff --git a/frontend/package.json b/frontend/package.json index e9cd68e4c4..959e2c20a7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,11 +11,13 @@ }, "dependencies": { "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 1c63261092..9d3d5512e0 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -56,6 +56,16 @@ const config: Config = { md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, + keyframes: { + progress: { + "0%": { transform: "translateX(0) scaleX(0)" }, + "40%": { transform: "translateX(0) scaleX(0.4)" }, + "100%": { transform: "translateX(100%) scaleX(0.5)" }, + }, + }, + animation: { + progress: "progress 1s infinite linear", + }, }, }, plugins: [require("tailwindcss-animate")], diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 90d4e96360..6c68e036b0 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -258,6 +258,20 @@ "@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-checkbox@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz#6465b800420923ecc39cbeaa8f357b5f09dbfd52" + integrity sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-presence" "1.1.1" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-use-previous" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/react-collection@1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz" @@ -546,6 +560,14 @@ dependencies: "@radix-ui/react-slot" "1.1.0" +"@radix-ui/react-progress@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.0.tgz#28c267885ec154fc557ec7a66cb462787312f7e2" + integrity sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg== + dependencies: + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-roving-focus@1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz" @@ -595,15 +617,7 @@ dependencies: "@radix-ui/react-primitive" "2.0.0" -"@radix-ui/react-slot@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" - integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-compose-refs" "1.0.1" - -"@radix-ui/react-slot@1.1.0": +"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz" integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw== @@ -1198,16 +1212,6 @@ client-only@0.0.1, client-only@^0.0.1: resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== -clsx@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz" - integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== - -clsx@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" - integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== - cmdk@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-1.0.0.tgz#0a095fdafca3dfabed82d1db78a6262fb163ded9" @@ -1216,6 +1220,11 @@ cmdk@1.0.0: "@radix-ui/react-dialog" "1.0.5" "@radix-ui/react-primitive" "1.0.3" +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"