diff options
author | 2024-08-11 13:18:04 -0700 | |
---|---|---|
committer | 2024-08-11 13:18:04 -0700 | |
commit | a9ec4cdbdab6f8026128728c34afa3150663a18b (patch) | |
tree | 093a91e2b05dec5a27dc373b71a038ebde9ee433 | |
parent | 32c8b0b24e868505eb46b3dbe2f3e29d467df2e4 (diff) | |
download | ibd-trader-a9ec4cdbdab6f8026128728c34afa3150663a18b.tar.gz ibd-trader-a9ec4cdbdab6f8026128728c34afa3150663a18b.tar.zst ibd-trader-a9ec4cdbdab6f8026128728c34afa3150663a18b.zip |
Add new-user screen
-rw-r--r-- | frontend/.gitmodules | 3 | ||||
-rw-r--r-- | frontend/.idea/codeStyles/Project.xml | 73 | ||||
-rw-r--r-- | frontend/.idea/codeStyles/codeStyleConfig.xml | 5 | ||||
-rw-r--r-- | frontend/.idea/vcs.xml | 1 | ||||
-rwxr-xr-x | frontend/bun.lockb | bin | 150832 -> 155194 bytes | |||
-rw-r--r-- | frontend/package.json | 10 | ||||
m--------- | frontend/src/api | 0 | ||||
-rw-r--r-- | frontend/src/app/api/new-user/add-ibd-creds/route.ts | 29 | ||||
-rw-r--r-- | frontend/src/app/api/new-user/add-ibd-creds/types.ts | 10 | ||||
-rw-r--r-- | frontend/src/app/api/new-user/check-ibd-creds/route.ts | 22 | ||||
-rw-r--r-- | frontend/src/app/api/new-user/verify-username/route.ts | 14 | ||||
-rw-r--r-- | frontend/src/app/api/new-user/verify-username/types.ts | 11 | ||||
-rw-r--r-- | frontend/src/app/dashboard/page.tsx | 31 | ||||
-rw-r--r-- | frontend/src/app/layout.tsx | 14 | ||||
-rw-r--r-- | frontend/src/app/new-user/page.tsx | 29 | ||||
-rw-r--r-- | frontend/src/client/client.ts | 23 | ||||
-rw-r--r-- | frontend/src/components/Layout/Layout.tsx | 55 | ||||
-rw-r--r-- | frontend/src/components/NewUserForm/NewUserForm.tsx | 171 | ||||
-rw-r--r-- | frontend/src/components/NewUserForm/styles.module.css | 13 |
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 Binary files differindex eddb205..8d89cf8 100755 --- a/frontend/bun.lockb +++ b/frontend/bun.lockb 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; +} |