aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Anshul Gupta <ansg191@anshulg.com> 2024-08-11 13:18:04 -0700
committerGravatar Anshul Gupta <ansg191@anshulg.com> 2024-08-11 13:18:04 -0700
commita9ec4cdbdab6f8026128728c34afa3150663a18b (patch)
tree093a91e2b05dec5a27dc373b71a038ebde9ee433
parent32c8b0b24e868505eb46b3dbe2f3e29d467df2e4 (diff)
downloadibd-trader-a9ec4cdbdab6f8026128728c34afa3150663a18b.tar.gz
ibd-trader-a9ec4cdbdab6f8026128728c34afa3150663a18b.tar.zst
ibd-trader-a9ec4cdbdab6f8026128728c34afa3150663a18b.zip
Add new-user screen
-rw-r--r--frontend/.gitmodules3
-rw-r--r--frontend/.idea/codeStyles/Project.xml73
-rw-r--r--frontend/.idea/codeStyles/codeStyleConfig.xml5
-rw-r--r--frontend/.idea/vcs.xml1
-rwxr-xr-xfrontend/bun.lockbbin150832 -> 155194 bytes
-rw-r--r--frontend/package.json10
m---------frontend/src/api0
-rw-r--r--frontend/src/app/api/new-user/add-ibd-creds/route.ts29
-rw-r--r--frontend/src/app/api/new-user/add-ibd-creds/types.ts10
-rw-r--r--frontend/src/app/api/new-user/check-ibd-creds/route.ts22
-rw-r--r--frontend/src/app/api/new-user/verify-username/route.ts14
-rw-r--r--frontend/src/app/api/new-user/verify-username/types.ts11
-rw-r--r--frontend/src/app/dashboard/page.tsx31
-rw-r--r--frontend/src/app/layout.tsx14
-rw-r--r--frontend/src/app/new-user/page.tsx29
-rw-r--r--frontend/src/client/client.ts23
-rw-r--r--frontend/src/components/Layout/Layout.tsx55
-rw-r--r--frontend/src/components/NewUserForm/NewUserForm.tsx171
-rw-r--r--frontend/src/components/NewUserForm/styles.module.css13
19 files changed, 502 insertions, 12 deletions
diff --git a/frontend/.gitmodules b/frontend/.gitmodules
new file mode 100644
index 0000000..2d6c487
--- /dev/null
+++ b/frontend/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "src/api"]
+ path = src/api
+ url = https://github.com/ansg191/ibd-trader-api.git
diff --git a/frontend/.idea/codeStyles/Project.xml b/frontend/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..4ae23e4
--- /dev/null
+++ b/frontend/.idea/codeStyles/Project.xml
@@ -0,0 +1,73 @@
+<component name="ProjectCodeStyleConfiguration">
+ <code_scheme name="Project" version="173">
+ <AndroidXmlCodeStyleSettings>
+ <option name="USE_CUSTOM_SETTINGS" value="true" />
+ </AndroidXmlCodeStyleSettings>
+ <HTMLCodeStyleSettings>
+ <option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
+ </HTMLCodeStyleSettings>
+ <JSCodeStyleSettings version="0">
+ <option name="FORCE_SEMICOLON_STYLE" value="true" />
+ <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
+ <option name="FORCE_QUOTE_STYlE" value="true" />
+ <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
+ <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
+ <option name="SPACES_WITHIN_IMPORTS" value="true" />
+ </JSCodeStyleSettings>
+ <JetCodeStyleSettings>
+ <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+ </JetCodeStyleSettings>
+ <TypeScriptCodeStyleSettings version="0">
+ <option name="FORCE_SEMICOLON_STYLE" value="true" />
+ <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
+ <option name="FORCE_QUOTE_STYlE" value="true" />
+ <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
+ <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
+ <option name="SPACES_WITHIN_IMPORTS" value="true" />
+ </TypeScriptCodeStyleSettings>
+ <VueCodeStyleSettings>
+ <option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
+ <option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
+ </VueCodeStyleSettings>
+ <codeStyleSettings language="HCL-Terraform">
+ <indentOptions>
+ <option name="TAB_SIZE" value="2" />
+ <option name="USE_TAB_CHARACTER" value="true" />
+ <option name="SMART_TABS" value="true" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="HTML">
+ <option name="SOFT_MARGINS" value="80" />
+ <indentOptions>
+ <option name="INDENT_SIZE" value="2" />
+ <option name="CONTINUATION_INDENT_SIZE" value="2" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="JavaScript">
+ <option name="SOFT_MARGINS" value="80" />
+ <indentOptions>
+ <option name="INDENT_SIZE" value="2" />
+ <option name="CONTINUATION_INDENT_SIZE" value="2" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="TypeScript">
+ <option name="SOFT_MARGINS" value="80" />
+ <indentOptions>
+ <option name="INDENT_SIZE" value="2" />
+ <option name="CONTINUATION_INDENT_SIZE" value="2" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="Vue">
+ <option name="SOFT_MARGINS" value="80" />
+ <indentOptions>
+ <option name="CONTINUATION_INDENT_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="kotlin">
+ <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+ </codeStyleSettings>
+ </code_scheme>
+</component> \ No newline at end of file
diff --git a/frontend/.idea/codeStyles/codeStyleConfig.xml b/frontend/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/frontend/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+ <state>
+ <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+ </state>
+</component> \ No newline at end of file
diff --git a/frontend/.idea/vcs.xml b/frontend/.idea/vcs.xml
index 35eb1dd..dc5671c 100644
--- a/frontend/.idea/vcs.xml
+++ b/frontend/.idea/vcs.xml
@@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
+ <mapping directory="$PROJECT_DIR$/src/api" vcs="Git" />
</component>
</project> \ No newline at end of file
diff --git a/frontend/bun.lockb b/frontend/bun.lockb
index eddb205..8d89cf8 100755
--- a/frontend/bun.lockb
+++ b/frontend/bun.lockb
Binary files differ
diff --git a/frontend/package.json b/frontend/package.json
index af55501..79fa2bb 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -7,14 +7,22 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
+ "generate": "make -C src/api generate-js",
"prettier-check": "prettier --check .",
"prettier-write": "prettier --write ."
},
"dependencies": {
"@auth0/nextjs-auth0": "^3.5.0",
+ "@bufbuild/protobuf": "^1.10.0",
+ "@connectrpc/connect-node": "^1.4.0",
+ "@fortawesome/fontawesome-svg-core": "^6.6.0",
+ "@fortawesome/free-solid-svg-icons": "^6.6.0",
+ "@fortawesome/react-fontawesome": "^0.2.2",
"next": "14.2.5",
"react": "^18",
- "react-dom": "^18"
+ "react-dom": "^18",
+ "server-only": "^0.0.1",
+ "zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20",
diff --git a/frontend/src/api b/frontend/src/api
new file mode 160000
+Subproject 250159ea00457090d9cc7b3cc270618d14ce9a9
diff --git a/frontend/src/app/api/new-user/add-ibd-creds/route.ts b/frontend/src/app/api/new-user/add-ibd-creds/route.ts
new file mode 100644
index 0000000..3740b90
--- /dev/null
+++ b/frontend/src/app/api/new-user/add-ibd-creds/route.ts
@@ -0,0 +1,29 @@
+import { NextRequest, NextResponse } from "next/server";
+import { createUserServiceClient } from "@/client/client";
+import { RequestBody, ResponseBody } from "./types";
+import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0";
+
+export const PUT = withApiAuthRequired(async function(req: NextRequest) {
+ const res = new NextResponse();
+ const session = await getSession(req, res);
+ if (!session || !session.user["sub"]) {
+ return res;
+ }
+
+ const { username, password } = RequestBody.parse(await req.json());
+
+ const client = createUserServiceClient();
+ await client.updateUser({
+ user: {
+ subject: session.user.sub,
+ ibdUsername: username,
+ ibdPassword: password,
+ },
+ updateMask: {
+ paths: ["ibd_username", "ibd_password"],
+ },
+ });
+
+ const ret: ResponseBody = {};
+ return NextResponse.json(ret, { status: 200 });
+});
diff --git a/frontend/src/app/api/new-user/add-ibd-creds/types.ts b/frontend/src/app/api/new-user/add-ibd-creds/types.ts
new file mode 100644
index 0000000..19b25fc
--- /dev/null
+++ b/frontend/src/app/api/new-user/add-ibd-creds/types.ts
@@ -0,0 +1,10 @@
+import { z } from "zod";
+
+export const RequestBody = z.object({
+ username: z.string(),
+ password: z.string(),
+});
+export const ResponseBody = z.object({});
+
+export type RequestBody = z.infer<typeof RequestBody>;
+export type ResponseBody = z.infer<typeof ResponseBody>;
diff --git a/frontend/src/app/api/new-user/check-ibd-creds/route.ts b/frontend/src/app/api/new-user/check-ibd-creds/route.ts
new file mode 100644
index 0000000..837f13a
--- /dev/null
+++ b/frontend/src/app/api/new-user/check-ibd-creds/route.ts
@@ -0,0 +1,22 @@
+import { NextRequest, NextResponse } from "next/server";
+import { createUserServiceClient } from "@/client/client";
+import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0";
+
+export const PUT = withApiAuthRequired(async function(req: NextRequest) {
+ const res = new NextResponse();
+ const session = await getSession(req, res);
+ if (!session || !session.user["sub"]) {
+ return res;
+ }
+
+ const client = createUserServiceClient();
+ const { authenticated } = await client.authenticateUser({
+ subject: session.user["sub"],
+ });
+
+ if (authenticated) {
+ return new Response(null, { status: 200 });
+ } else {
+ return new Response(null, { status: 401 });
+ }
+});
diff --git a/frontend/src/app/api/new-user/verify-username/route.ts b/frontend/src/app/api/new-user/verify-username/route.ts
new file mode 100644
index 0000000..b60c76e
--- /dev/null
+++ b/frontend/src/app/api/new-user/verify-username/route.ts
@@ -0,0 +1,14 @@
+import { NextRequest, NextResponse } from "next/server";
+import { createUserServiceClient } from "@/client/client";
+import { RequestBody, ResponseBody } from "@/app/api/new-user/verify-username/types";
+import { withApiAuthRequired } from "@auth0/nextjs-auth0";
+
+export const POST = withApiAuthRequired(async function(request: NextRequest) {
+ const { username } = RequestBody.parse(await request.json());
+
+ const client = createUserServiceClient();
+ const { exists } = await client.checkIBDUsername({ ibdUsername: username });
+
+ const ret: ResponseBody = { exists };
+ return NextResponse.json(ret, { status: 200 });
+});
diff --git a/frontend/src/app/api/new-user/verify-username/types.ts b/frontend/src/app/api/new-user/verify-username/types.ts
new file mode 100644
index 0000000..5063da4
--- /dev/null
+++ b/frontend/src/app/api/new-user/verify-username/types.ts
@@ -0,0 +1,11 @@
+import { z } from "zod";
+
+export const RequestBody = z.object({
+ username: z.string(),
+});
+export const ResponseBody = z.object({
+ exists: z.boolean(),
+});
+
+export type RequestBody = z.infer<typeof RequestBody>;
+export type ResponseBody = z.infer<typeof ResponseBody>;
diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx
index 8ca17c6..289dcab 100644
--- a/frontend/src/app/dashboard/page.tsx
+++ b/frontend/src/app/dashboard/page.tsx
@@ -1,15 +1,34 @@
import { Metadata } from "next";
-import Link from "next/link";
+import { createUserServiceClient } from "@/client/client";
+import { getSession, withPageAuthRequired } from "@auth0/nextjs-auth0";
+import Layout from "@/components/Layout/Layout";
+import { redirect, RedirectType } from "next/navigation";
+import { red } from "next/dist/lib/picocolors";
export const metadata: Metadata = {
title: "IBD Trader Dashboard",
};
-export default function Dashboard() {
+export default withPageAuthRequired(async function Dashboard() {
+ const client = createUserServiceClient();
+
+ const user = await getSession();
+ if (!user) {
+ redirect("/api/auth/login", RedirectType.replace);
+ }
+
+ const dbUser = await client.createUser({
+ subject: user.user["sub"],
+ });
+
+ // Check if user has IBD credentials
+ if (!dbUser.user || !dbUser.user.ibdUsername) {
+ redirect("/new-user", RedirectType.replace);
+ }
+
return (
- <div>
+ <Layout>
<h1>Dashboard</h1>
- <Link href="/api/auth/logout">Logout</Link>
- </div>
+ </Layout>
);
-}
+}, { returnTo: "/dashboard" });
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 7eae606..bb15d68 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -2,6 +2,10 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { UserProvider } from "@auth0/nextjs-auth0/client";
+import { config } from "@fortawesome/fontawesome-svg-core";
+import "@fortawesome/fontawesome-svg-core/styles.css";
+
+config.autoAddCss = false;
const inter = Inter({ subsets: ["latin"] });
@@ -11,13 +15,13 @@ export const metadata: Metadata = {
};
export default function RootLayout({
- children,
-}: Readonly<{ children: React.ReactNode }>) {
+ children,
+ }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
- <UserProvider>
- <body className={inter.className}>{children}</body>
- </UserProvider>
+ <UserProvider>
+ <body className={inter.className}>{children}</body>
+ </UserProvider>
</html>
);
}
diff --git a/frontend/src/app/new-user/page.tsx b/frontend/src/app/new-user/page.tsx
new file mode 100644
index 0000000..57fcf82
--- /dev/null
+++ b/frontend/src/app/new-user/page.tsx
@@ -0,0 +1,29 @@
+import { Metadata } from "next";
+import { getSession, withPageAuthRequired } from "@auth0/nextjs-auth0";
+import { redirect, RedirectType } from "next/navigation";
+import { createUserServiceClient } from "@/client/client";
+import NewUserForm from "@/components/NewUserForm/NewUserForm";
+
+export const metadata: Metadata = {
+ title: "New User",
+};
+
+export default withPageAuthRequired(async function NewUser() {
+ const session = await getSession();
+ if (!session) {
+ redirect("/api/auth/login", RedirectType.replace);
+ }
+
+ const client = createUserServiceClient();
+ const { user } = await client.getUser({ subject: session.user["sub"] });
+ if (!user) {
+ throw new Error("User not found");
+ }
+
+ if (user.ibdUsername) {
+ // User already has IBD credentials
+ redirect("/dashboard", RedirectType.replace);
+ }
+
+ return <NewUserForm />;
+});
diff --git a/frontend/src/client/client.ts b/frontend/src/client/client.ts
new file mode 100644
index 0000000..7a92917
--- /dev/null
+++ b/frontend/src/client/client.ts
@@ -0,0 +1,23 @@
+import "server-only";
+import { createGrpcTransport } from "@connectrpc/connect-node";
+import { createPromiseClient, PromiseClient } from "@connectrpc/connect";
+import { UserService } from "@/api/gen/idb/user/v1/user_connect";
+import { StockService } from "@/api/gen/idb/stock/v1/stock_connect";
+
+const baseUrl = process.env.BACKEND_URL || "http://localhost:8000";
+const transport = createGrpcTransport({
+ baseUrl,
+ httpVersion: "2",
+});
+
+export type UserServiceClient = PromiseClient<typeof UserService>;
+
+export function createUserServiceClient(): UserServiceClient {
+ return createPromiseClient(UserService, transport);
+}
+
+export type StockServiceClient = PromiseClient<typeof StockService>
+
+export function createStockServiceClient(): StockServiceClient {
+ return createPromiseClient(StockService, transport);
+}
diff --git a/frontend/src/components/Layout/Layout.tsx b/frontend/src/components/Layout/Layout.tsx
new file mode 100644
index 0000000..6617038
--- /dev/null
+++ b/frontend/src/components/Layout/Layout.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+import React, { useState } from "react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+const paths = [
+ { href: "/dashboard", label: "Home" },
+ { href: "/dashboard/positions", label: "Positions" },
+];
+
+export type LayoutProps = Readonly<{
+ children: React.ReactNode;
+}>;
+
+export default function Layout({ children }: LayoutProps) {
+ const [isOpen, setIsOpen] = useState(true);
+ const path = usePathname();
+
+ return (
+ <div className="flex h-screen bg-gray-100">
+ {/* Sidebar */}
+ <div
+ className={`relative text-black space-y-6 py-7 px-2 transition-all duration-500 ease-in-out ${isOpen ? "w-64" : "w-12 overflow-hidden"}`}>
+ {/* Sidebar content */}
+ <nav className={isOpen ? "" : "hidden"}>
+ {paths.map(({ href, label }) => (
+ <Link key={href} href={href}
+ className={`block py-2.5 px-4 mb-2 rounded transition duration-200 ${href == path ? "bg-blue-200" : "hover:bg-blue-200"}`}>
+ {label}
+ </Link>
+ ))}
+ </nav>
+ {/* Toggle button */}
+ <div className="absolute bottom-2 right-0 p-2">
+ <button onClick={() => setIsOpen(!isOpen)}
+ className="text-gray-700 focus:outline-none transition-transform duration-500 transform"
+ style={{ transform: isOpen ? "rotate(180deg)" : "rotate(0)" }}
+ >
+ <FontAwesomeIcon icon={faArrowRight} size="2x" />
+ </button>
+ </div>
+ </div>
+
+ {/* Main content */}
+ <div className="flex-1 flex flex-col overflow-hidden">
+ <main className="flex-1 overflow-y-auto p-4">
+ {children}
+ </main>
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/src/components/NewUserForm/NewUserForm.tsx b/frontend/src/components/NewUserForm/NewUserForm.tsx
new file mode 100644
index 0000000..779a49f
--- /dev/null
+++ b/frontend/src/components/NewUserForm/NewUserForm.tsx
@@ -0,0 +1,171 @@
+"use client";
+
+import { User } from "@/api/gen/idb/user/v1/user_pb";
+import { ChangeEventHandler, FormEventHandler, MouseEventHandler, useEffect, useState } from "react";
+import {
+ RequestBody as VerifyRequestBody,
+ ResponseBody as VerifyResponseBody,
+} from "@/app/api/new-user/verify-username/types";
+import {
+ RequestBody as SubmitRequestBody,
+ ResponseBody as SubmitResponseBody,
+} from "@/app/api/new-user/add-ibd-creds/types";
+
+import styles from "./styles.module.css";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faSpinner } from "@fortawesome/free-solid-svg-icons";
+
+export default function NewUserForm() {
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const [isUsernameValid, setIsUsernameValid] = useState(false);
+ const [isSubmittingUsername, setIsSubmittingUsername] = useState(false);
+ const [isSubmittingForm, setIsSubmittingForm] = useState(false);
+ const [isCheckingCreds, setIsCheckingCreds] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+
+ const handleUsernameSubmit: FormEventHandler<HTMLFormElement> = (e) => {
+ e.preventDefault();
+ setIsSubmittingUsername(true);
+ setError(null);
+
+ const body: VerifyRequestBody = { username };
+ fetch("/api/new-user/verify-username", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ }).then((res) => res.json())
+ .then((data) => {
+ const { exists } = VerifyResponseBody.parse(data);
+ if (exists) {
+ setIsUsernameValid(true);
+ } else {
+ setError("Username does not exist");
+ }
+ setIsSubmittingUsername(false);
+ });
+ };
+ const handleFormSubmit: FormEventHandler<HTMLFormElement> = (e) => {
+ e.preventDefault();
+ setIsSubmittingForm(true);
+ setError(null);
+
+ const body: SubmitRequestBody = { username, password };
+ fetch("/api/new-user/add-ibd-creds", {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ }).then((res) => res.json())
+ .then((data) => {
+ const {} = SubmitResponseBody.parse(data);
+ setIsSubmittingForm(false);
+ setIsCheckingCreds(true);
+
+ return fetch("/api/new-user/check-ibd-creds", {
+ method: "PUT",
+ });
+ })
+ .then((res) => {
+ if (res.status === 200) {
+ window.location.href = "/dashboard";
+ } else {
+ setError("Invalid credentials");
+ setIsCheckingCreds(false);
+ }
+ });
+ };
+
+ const handleBack: MouseEventHandler<HTMLButtonElement> = () => {
+ setIsUsernameValid(false);
+ setPassword("");
+ setError(null);
+ };
+
+ return (
+ <div className="flex items-center justify-center min-h-screen bg-gray-100">
+ <div className="w-full max-w-md p-8 space-y-4 bg-white rounded-lg shadow-lg">
+ <h1 className="text-2xl font-bold text-center">Set Up Your Account</h1>
+
+ <form onSubmit={isUsernameValid ? handleFormSubmit : handleUsernameSubmit}>
+ <div className="mb-4">
+ <label htmlFor="username" className="block text-sm font-medium text-gray-700">
+ Username
+ </label>
+ <input
+ type="text"
+ id="username"
+ value={username}
+ onChange={(e) => setUsername(e.target.value)}
+ className={`w-full px-3 py-2 mt-1 border ${
+ isUsernameValid ? "border-gray-300 bg-gray-200" : "border-gray-300"
+ } rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500`}
+ required
+ disabled={isUsernameValid}
+ />
+ {error && !isUsernameValid && (
+ <p className="mt-2 text-sm text-red-600">{error}</p>
+ )}
+ </div>
+ {!isUsernameValid && (
+ <div className="flex justify-center">
+ <button
+ type="submit"
+ className="px-4 py-2 text-white bg-indigo-600 rounded-md shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
+ disabled={isSubmittingUsername}
+ >
+ {isSubmittingUsername ? <FontAwesomeIcon icon={faSpinner} spin /> : "Next"}
+ </button>
+ </div>
+ )}
+ </form>
+
+ {isUsernameValid && (
+ <form onSubmit={handleFormSubmit}>
+ <div className={styles.passwordForm}>
+ <div className="mb-4">
+ <label htmlFor="password" className="block text-sm font-medium text-gray-700">
+ Password
+ </label>
+ <input
+ type="password"
+ id="password"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ className="w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
+ required
+ />
+ </div>
+ {error && isUsernameValid && (
+ <p className="mt-2 text-sm text-red-600">{error}</p>
+ )}
+ <div className="flex justify-between">
+ <button
+ type="button"
+ onClick={handleBack}
+ className="px-4 py-2 text-white bg-gray-600 rounded-md shadow-sm hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
+ >
+ Back
+ </button>
+ <button
+ type="submit"
+ className="px-4 py-2 text-white bg-indigo-600 rounded-md shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
+ disabled={isSubmittingForm}
+ >
+ {isSubmittingForm ? (
+ <p><FontAwesomeIcon icon={faSpinner} spin /> Setting Credentials...</p>
+ ) : isCheckingCreds ? (
+ <p><FontAwesomeIcon icon={faSpinner} spin /> Checking Credentials...</p>
+ ) : "Submit"}
+ </button>
+ </div>
+ </div>
+ </form>
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/src/components/NewUserForm/styles.module.css b/frontend/src/components/NewUserForm/styles.module.css
new file mode 100644
index 0000000..a2ce597
--- /dev/null
+++ b/frontend/src/components/NewUserForm/styles.module.css
@@ -0,0 +1,13 @@
+@keyframes smooth-appear {
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.passwordForm {
+ margin-top: 1rem;
+ transform: translateY(-5rem);
+ opacity: 0;
+ animation: smooth-appear 1s ease forwards;
+}