diff --git a/client/package-lock.json b/client/package-lock.json
index e7aaaa4..a7740b5 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -15,6 +15,7 @@
"@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-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"@tanstack/react-query": "^5.45.1",
@@ -41,6 +42,7 @@
"sonner": "^1.6.1",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
+ "validator": "^13.12.0",
"zod": "^3.23.8",
"zustand": "^5.0.1",
"zustand-persist": "^0.4.0"
@@ -52,6 +54,7 @@
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
+ "@types/validator": "^13.12.2",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"eslint": "^8.57.0",
@@ -1403,25 +1406,107 @@
}
},
"node_modules/@radix-ui/react-popover": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz",
- "integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz",
+ "integrity": "sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==",
+ "license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
- "@radix-ui/react-context": "1.1.0",
- "@radix-ui/react-dismissable-layer": "1.1.0",
- "@radix-ui/react-focus-guards": "1.1.0",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.1",
+ "@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.0",
- "@radix-ui/react-portal": "1.1.1",
- "@radix-ui/react-presence": "1.1.0",
+ "@radix-ui/react-portal": "1.1.2",
+ "@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.1.1",
- "react-remove-scroll": "2.5.7"
+ "react-remove-scroll": "2.6.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
+ "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz",
+ "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-escape-keydown": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
+ "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
+ "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
@@ -1438,6 +1523,55 @@
}
}
},
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
+ "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/react-remove-scroll": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz",
+ "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.6",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-popper": {
"version": "1.2.0",
"license": "MIT",
@@ -2217,6 +2351,13 @@
"@types/react": "*"
}
},
+ "node_modules/@types/validator": {
+ "version": "13.12.2",
+ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
+ "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz",
@@ -8206,6 +8347,15 @@
"version": "1.0.2",
"license": "MIT"
},
+ "node_modules/validator": {
+ "version": "13.12.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
+ "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"license": "ISC",
diff --git a/client/package.json b/client/package.json
index 1245101..ee65af0 100644
--- a/client/package.json
+++ b/client/package.json
@@ -15,14 +15,14 @@
"prepare": "cd .. && husky client/.husky"
},
"dependencies": {
- "@hookform/resolvers": "^3.9.0",
+ "@hookform/resolvers": "^3.9.1",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
- "@radix-ui/react-popover": "^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-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"@tanstack/react-query": "^5.45.1",
@@ -44,11 +44,12 @@
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
- "react-hook-form": "^7.52.1",
+ "react-hook-form": "^7.53.1",
"react-icons": "^5.2.1",
"sonner": "^1.6.1",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
+ "validator": "^13.12.0",
"zod": "^3.23.8",
"zustand": "^5.0.1",
"zustand-persist": "^0.4.0"
@@ -60,6 +61,7 @@
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
+ "@types/validator": "^13.12.2",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"eslint": "^8.57.0",
diff --git a/client/src/components/main/EventPage.tsx b/client/src/components/main/EventPage.tsx
index 1bc07a5..e2afc83 100644
--- a/client/src/components/main/EventPage.tsx
+++ b/client/src/components/main/EventPage.tsx
@@ -6,9 +6,9 @@ import Link from "next/link";
import RsvpButton from "@/components/main/RsvpButton";
import RsvpListModal from "@/components/main/RsvpListModal";
+import SignUpModal from "@/components/modal/sign-up";
import LogInModal from "@/components/ui/LogInModal";
import PageCard from "@/components/ui/page-card";
-import SignUpModal from "@/components/ui/SignUpModal";
import { useUser } from "@/hooks/useUser";
import type { Event } from "@/types/event";
diff --git a/client/src/components/main/header/Navbar.tsx b/client/src/components/main/header/Navbar.tsx
index fd219dc..bd8e427 100644
--- a/client/src/components/main/header/Navbar.tsx
+++ b/client/src/components/main/header/Navbar.tsx
@@ -1,9 +1,8 @@
import Image from "next/image";
import Link from "next/link";
-import { useState } from "react";
+import SignUpModal from "@/components/modal/sign-up";
import LogInModal from "@/components/ui/LogInModal";
-import SignUpModal from "@/components/ui/SignUpModal";
import { useAuth } from "@/context/AuthProvider";
import { DropDownNav } from "./DropDown";
diff --git a/client/src/components/modal/sign-up.tsx b/client/src/components/modal/sign-up.tsx
new file mode 100644
index 0000000..57fefc6
--- /dev/null
+++ b/client/src/components/modal/sign-up.tsx
@@ -0,0 +1,253 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { ReloadIcon } from "@radix-ui/react-icons";
+import { useRouter } from "next/router";
+import { ReactNode } from "react";
+import { useForm } from "react-hook-form";
+import { toast } from "sonner";
+import validator from "validator";
+import { z } from "zod";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/BetterDialog";
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormInput,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import LogInModal from "@/components/ui/LogInModal";
+import { SelectBranch } from "@/components/ui/select-branch";
+import { useAuth } from "@/context/AuthProvider";
+import { useDelayedPending } from "@/hooks/useDelayedPending";
+import { useRegister } from "@/hooks/useUser";
+
+const schema = z
+ .object({
+ email: z.string().email(),
+ phone: z
+ .string()
+ .refine(validator.isMobilePhone, "Must be a valid phone number"),
+ first_name: z.string().min(1).max(255),
+ last_name: z.string().min(1).max(255),
+ password: z.string().min(6),
+ password_confirm: z.string().min(6),
+ branch_id: z.number(),
+ })
+ .superRefine(({ password, password_confirm }, ctx) => {
+ if (password != password_confirm) {
+ ctx.addIssue({
+ code: "custom",
+ message: "Passwords must match",
+ path: ["password_confirm"],
+ });
+ }
+ });
+
+type Props = {
+ children: ReactNode;
+};
+
+export default function SignUpModal({ children }: Props) {
+ return (
+
+ );
+}
+
+function SignUpForm() {
+ const router = useRouter();
+ const { login } = useAuth();
+ const { mutate: register, isPending } = useRegister({
+ onSuccess: (_, details) => {
+ login(details.email, details.password).then(() => {
+ router.push("/profile");
+ toast.success("Your account has been created.");
+ });
+ },
+ onError: (error) => {
+ const errorMessage = error.response?.data?.["error"];
+
+ if (
+ typeof errorMessage === "string" &&
+ errorMessage.includes("duplicate")
+ ) {
+ form.setError("email", { message: "Email is already in use" });
+ } else {
+ toast.error("Something went wrong. Please try again later.");
+ }
+ },
+ });
+
+ const showIsPending = useDelayedPending(isPending);
+
+ const form = useForm