Repository: djeck1432/spotnet
Pull request #740: add login and forgot password components
Author: login: kilinich-v
Created: 2025-03-21T15:57:47Z | Merged: 2025-03-26T07:49:48Z



Diff:
diff --git a/margin_frontend/src/routeTree.gen.ts b/margin_frontend/src/routeTree.gen.ts
index 2a9359c01..5a6aad310 100644
--- a/margin_frontend/src/routeTree.gen.ts
+++ b/margin_frontend/src/routeTree.gen.ts
@@ -13,6 +13,7 @@
 import { Route as rootRoute } from './routes/__root'
 import { Route as TradeImport } from './routes/trade'
 import { Route as PoolImport } from './routes/pool'
+import { Route as LoginImport } from './routes/login'
 import { Route as ChangePasswordImport } from './routes/change-password'
 import { Route as IndexImport } from './routes/index'
 
@@ -24,13 +25,18 @@ const TradeRoute = TradeImport.update({
   getParentRoute: () => rootRoute,
 } as any)
 
-
 const PoolRoute = PoolImport.update({
   id: '/pool',
   path: '/pool',
   getParentRoute: () => rootRoute,
 } as any)
 
+const LoginRoute = LoginImport.update({
+  id: '/login',
+  path: '/login',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const ChangePasswordRoute = ChangePasswordImport.update({
   id: '/change-password',
   path: '/change-password',
@@ -61,6 +67,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof ChangePasswordImport
       parentRoute: typeof rootRoute
     }
+    '/login': {
+      id: '/login'
+      path: '/login'
+      fullPath: '/login'
+      preLoaderRoute: typeof LoginImport
+      parentRoute: typeof rootRoute
+    }
     '/pool': {
       id: '/pool'
       path: '/pool'
@@ -83,6 +96,7 @@ declare module '@tanstack/react-router' {
 export interface FileRoutesByFullPath {
   '/': typeof IndexRoute
   '/change-password': typeof ChangePasswordRoute
+  '/login': typeof LoginRoute
   '/pool': typeof PoolRoute
   '/trade': typeof TradeRoute
 }
@@ -90,6 +104,7 @@ export interface FileRoutesByFullPath {
 export interface FileRoutesByTo {
   '/': typeof IndexRoute
   '/change-password': typeof ChangePasswordRoute
+  '/login': typeof LoginRoute
   '/pool': typeof PoolRoute
   '/trade': typeof TradeRoute
 }
@@ -98,22 +113,24 @@ export interface FileRoutesById {
   __root__: typeof rootRoute
   '/': typeof IndexRoute
   '/change-password': typeof ChangePasswordRoute
+  '/login': typeof LoginRoute
   '/pool': typeof PoolRoute
   '/trade': typeof TradeRoute
 }
 
 export interface FileRouteTypes {
   fileRoutesByFullPath: FileRoutesByFullPath
-  fullPaths: '/' | '/change-password' | '/pool' | '/trade'
+  fullPaths: '/' | '/change-password' | '/login' | '/pool' | '/trade'
   fileRoutesByTo: FileRoutesByTo
-  to: '/' | '/change-password' | '/pool' | '/trade'
-  id: '__root__' | '/' | '/change-password' | '/pool' | '/trade'
+  to: '/' | '/change-password' | '/login' | '/pool' | '/trade'
+  id: '__root__' | '/' | '/change-password' | '/login' | '/pool' | '/trade'
   fileRoutesById: FileRoutesById
 }
 
 export interface RootRouteChildren {
   IndexRoute: typeof IndexRoute
   ChangePasswordRoute: typeof ChangePasswordRoute
+  LoginRoute: typeof LoginRoute
   PoolRoute: typeof PoolRoute
   TradeRoute: typeof TradeRoute
 }
@@ -121,6 +138,7 @@ export interface RootRouteChildren {
 const rootRouteChildren: RootRouteChildren = {
   IndexRoute: IndexRoute,
   ChangePasswordRoute: ChangePasswordRoute,
+  LoginRoute: LoginRoute,
   PoolRoute: PoolRoute,
   TradeRoute: TradeRoute,
 }
@@ -137,6 +155,7 @@ export const routeTree = rootRoute
       "children": [
         "/",
         "/change-password",
+        "/login",
         "/pool",
         "/trade"
       ]
@@ -147,6 +166,9 @@ export const routeTree = rootRoute
     "/change-password": {
       "filePath": "change-password.tsx"
     },
+    "/login": {
+      "filePath": "login.tsx"
+    },
     "/pool": {
       "filePath": "pool.tsx"
     },
diff --git a/margin_frontend/src/routes/index.tsx b/margin_frontend/src/routes/index.tsx
index 2e305bb46..8069ff5ff 100644
--- a/margin_frontend/src/routes/index.tsx
+++ b/margin_frontend/src/routes/index.tsx
@@ -1,38 +1,38 @@
-import { createFileRoute } from "@tanstack/react-router";
-import { Button } from "../ui/core/button";
+import { createFileRoute } from '@tanstack/react-router';
+import { Button } from '../ui/core/button';
 
-export const Route = createFileRoute("/")({
-	component: Home,
+export const Route = createFileRoute('/')({
+  component: Home
 });
 
 function Home() {
-	return (
-		<div className="mt-[100px] md:mt-0 pb-[112px] md:pb-0">
-			<h1 className="hidden text-center uppercase md:block text-header-text-size leading-header-text-lineheight font-pilotCommandSpaced text-headerText">
-				Trade, Earn, Grow
-			</h1>
-			<div className="space-y-8 md:hidden">
-				<h1 className="text-center uppercase font-pilotCommandSpaced text-header-text-size leading-header-text-lineheight text-headerText">
-					Trade
-				</h1>
-				<div className="h-3.5 w-3.5 rounded-full bg-white mx-auto" />
-				<h1 className="text-center uppercase font-pilotCommandSpaced text-header-text-size leading-header-text-lineheight text-headerText">
-					Earn
-				</h1>
-				<div className="h-3.5 w-3.5 rounded-full bg-white mx-auto" />
-				<h1 className="text-center uppercase font-pilotCommandSpaced text-header-text-size leading-header-text-lineheight text-headerText">
-					Grow
-				</h1>
-			</div>
+  return (
+    <div className='mt-[100px] md:mt-0 pb-[112px] md:pb-0'>
+      <h1 className='hidden text-center uppercase md:block text-header-text-size leading-header-text-lineheight font-pilotCommandSpaced text-headerText'>
+        Trade, Earn, Grow
+      </h1>
+      <div className='space-y-8 md:hidden'>
+        <h1 className='text-center uppercase font-pilotCommandSpaced text-header-text-size leading-header-text-lineheight text-headerText'>
+          Trade
+        </h1>
+        <div className='h-3.5 w-3.5 rounded-full bg-white mx-auto' />
+        <h1 className='text-center uppercase font-pilotCommandSpaced text-header-text-size leading-header-text-lineheight text-headerText'>
+          Earn
+        </h1>
+        <div className='h-3.5 w-3.5 rounded-full bg-white mx-auto' />
+        <h1 className='text-center uppercase font-pilotCommandSpaced text-header-text-size leading-header-text-lineheight text-headerText'>
+          Grow
+        </h1>
+      </div>
 
-			<p className="w-10/12 mx-auto mt-4 text-sm font-medium text-center font-bricolageGrotesque md:text-base text-baseWhite">
-				Trade smarter, earn bigger, and grow your portfolio with confidence.
-			</p>
-			<div className="flex justify-center mt-12">
-				<Button variant={"outline"} size={"lg"} className="w-[219px]">
-					Launch app
-				</Button>
-			</div>
-		</div>
-	);
+      <p className='w-10/12 mx-auto mt-4 text-sm font-medium text-center font-bricolageGrotesque md:text-base text-baseWhite'>
+        Trade smarter, earn bigger, and grow your portfolio with confidence.
+      </p>
+      <div className='flex justify-center mt-12'>
+        <Button variant={'outline'} size={'lg'} className='w-[219px]'>
+          Launch app
+        </Button>
+      </div>
+    </div>
+  );
 }
diff --git a/margin_frontend/src/routes/login.tsx b/margin_frontend/src/routes/login.tsx
new file mode 100644
index 000000000..3c92d7933
--- /dev/null
+++ b/margin_frontend/src/routes/login.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router';
+import LoginForm from '../ui/views/LoginForm';
+
+export const Route = createFileRoute('/login')({
+  component: Login
+});
+
+function Login() {
+  return (
+    <div className='flex justify-center items-center'>
+      <LoginForm />
+    </div>
+  );
+}
diff --git a/margin_frontend/src/ui/views/LoginForm.tsx b/margin_frontend/src/ui/views/LoginForm.tsx
new file mode 100644
index 000000000..2c1674a8e
--- /dev/null
+++ b/margin_frontend/src/ui/views/LoginForm.tsx
@@ -0,0 +1,139 @@
+import { useState } from 'react';
+
+import { Input } from '../core/input';
+import { Card } from '../core/card';
+import { Button } from '../core/button';
+
+const textData = {
+  h1: 'Welcome back!',
+  hint: 'Please enter your credentials to sign in!',
+  email: 'Email',
+  emailPlaceholder: 'Enter your email',
+  password: 'Password',
+  passwordPlaceholder: 'Enter your password',
+  login: 'Sign In',
+  rememberMe: 'Remember Me',
+  forgotPassword: 'Forgot Password?',
+  signUp: 'Sign Up',
+  signUpHint: 'Don’t have an account yet?'
+};
+
+type LoginFormSchema = {
+  email: string;
+  password: string;
+  rememberMe: boolean;
+};
+
+const LoginForm = () => {
+  const [passwordVisibility, setPasswordVisibility] = useState(false);
+  const [email, setEmail] = useState('');
+  const [password, setPassword] = useState('');
+  const [rememberMe, setRememberMe] = useState(false);
+
+  const onSendForm = async () => {
+    const data: LoginFormSchema = {
+      email,
+      password,
+      rememberMe
+    };
+
+    console.log(data);
+  };
+
+  return (
+    <Card className='text-white flex gap-6 flex-col font-bricolageGrotesque'>
+      <div className='flex flex-col gap-2'>
+        <h1 className='text-2xl font-bold text-center'>{textData.h1}</h1>
+        <p className='text-center'>{textData.hint}</p>
+      </div>
+      <div className='flex flex-col'>
+        <label>{textData.email}</label>
+        <Input
+          className='w-100'
+          type='email'
+          placeholder={textData.emailPlaceholder}
+          onChange={e => setEmail(e.target.value)}
+          value={email}
+        />
+      </div>
+      <div className='flex flex-col'>
+        <label>{textData.password}</label>
+        <div className='w-100 relative flex items-center  justify-center'>
+          <Input
+            className='w-100 pr-10'
+            type={passwordVisibility ? 'text' : 'password'}
+            placeholder={textData.passwordPlaceholder}
+            onChange={e => setPassword(e.target.value)}
+            value={password}
+          />
+          <svg
+            onClick={() => setPasswordVisibility(!passwordVisibility)}
+            className='shrink-0 size-3.5 absolute right-4 cursor-pointer'
+            width='24'
+            height='24'
+            viewBox='0 0 24 24'
+            fill='none'
+            stroke='currentColor'
+            strokeWidth='2'
+            strokeLinecap='round'
+            strokeLinejoin='round'
+          >
+            <path
+              className='hs-password-active:hidden'
+              d='M9.88 9.88a3 3 0 1 0 4.24 4.24'
+            ></path>
+            <path
+              className='hs-password-active:hidden'
+              d='M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68'
+            ></path>
+            <path
+              className='hs-password-active:hidden'
+              d='M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61'
+            ></path>
+            <line
+              className={passwordVisibility ? 'hidden' : ''}
+              x1='2'
+              x2='22'
+              y1='2'
+              y2='22'
+            ></line>
+            <path
+              className={!passwordVisibility ? 'hidden' : ''}
+              d='M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z'
+            ></path>
+            <circle
+              className={!passwordVisibility ? 'hidden' : ''}
+              cx='12'
+              cy='12'
+              r='3'
+            ></circle>
+          </svg>
+        </div>
+      </div>
+      <div className='flex items-center gap-4 place-content-between text-xs'>
+        <label className='flex items-center gap-2'>
+          <input
+            type='checkbox'
+            checked={rememberMe}
+            onChange={() => setRememberMe(!rememberMe)}
+          />
+          {textData.rememberMe}
+        </label>
+        <a href='/change-password' className='text-xs underline'>
+          {textData.forgotPassword}
+        </a>
+      </div>
+      <Button variant={'outline'} onClick={onSendForm}>
+        {textData.login}
+      </Button>
+      <div className='flex justify-center gap-2 text-xs'>
+        <p>{textData.signUpHint}</p>
+        <a href='#' className='underline'>
+          {textData.signUp}
+        </a>
+      </div>
+    </Card>
+  );
+};
+
+export default LoginForm;


Reviews:
user: login: vadim-ghostman
body: please make those changes in `margin_frontend/src` directory, not in `margin_frontend/admin_template` directory

you can look [here](https://github.com/djeck1432/spotnet/pull/743) for guidance which things should be changed

also, you can look [here](https://elstar.themenate.net/sign-in) for layout example and fields should be `email` and `password` with reference on forgot password page (can be a stub, that will be replaced when forgot password page will be merged)

about the code part, I don't see any labels above the inputs
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-23T16:29:11Z
user: login: vadim-ghostman
body: now it's better, but here are some improvements
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T21:41:58Z
user: login: vadim-ghostman
body: only one grammar error and that's all
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T08:35:15Z
user: login: vadim-ghostman
body: good job
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-25T17:58:12Z
user: login: djeck1432
body: LGTM
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-26T07:49:42Z

Review comments:
diff_hunk: @@ -40,94 +48,106 @@ const ChangePasswordRoute = ChangePasswordImport.update({
 const IndexRoute = IndexImport.update({
   id: '/',
   path: '/',
-  getParentRoute: () => rootRoute,
-} as any)
+  getParentRoute: () => rootRoute
+} as any);
 
 // Populate the FileRoutesByPath interface
 
 declare module '@tanstack/react-router' {
   interface FileRoutesByPath {
     '/': {
-      id: '/'
-      path: '/'
-      fullPath: '/'
-      preLoaderRoute: typeof IndexImport
-      parentRoute: typeof rootRoute
-    }
-    '/change-password': {
+      id: '/';
+      path: '/';
+      fullPath: '/';
+      preLoaderRoute: typeof IndexImport;
+      parentRoute: typeof rootRoute;
+    };
+    '/login': {
+      id: '/login';
+      path: '/login';
+      fullPath: '/login';
+      preLoaderRoute: typeof LoginImport;
+      parentRoute: typeof rootRoute;
+    };
+     '/change-password': {
       id: '/change-password'
       path: '/change-password'
       fullPath: '/change-password'
       preLoaderRoute: typeof ChangePasswordImport
       parentRoute: typeof rootRoute
     }
     '/pool': {
-      id: '/pool'
-      path: '/pool'
-      fullPath: '/pool'
-      preLoaderRoute: typeof PoolImport
-      parentRoute: typeof rootRoute
-    }
+      id: '/pool';
+      path: '/pool';
+      fullPath: '/pool';
+      preLoaderRoute: typeof PoolImport;
+      parentRoute: typeof rootRoute;
+    };
     '/trade': {
-      id: '/trade'
-      path: '/trade'
-      fullPath: '/trade'
-      preLoaderRoute: typeof TradeImport
-      parentRoute: typeof rootRoute
-    }
+      id: '/trade';
+      path: '/trade';
+      fullPath: '/trade';
+      preLoaderRoute: typeof TradeImport;
+      parentRoute: typeof rootRoute;
+    };
   }
 }
 
 // Create and export the route tree
 
 export interface FileRoutesByFullPath {
-  '/': typeof IndexRoute
+  '/': typeof IndexRoute;
+  '/login': typeof LoginRoute;
   '/change-password': typeof ChangePasswordRoute
-  '/pool': typeof PoolRoute
-  '/trade': typeof TradeRoute
+  '/pool': typeof PoolRoute;
+  '/trade': typeof TradeRoute;
 }
 
 export interface FileRoutesByTo {
-  '/': typeof IndexRoute
+  '/': typeof IndexRoute;
+  '/login': typeof LoginRoute;
   '/change-password': typeof ChangePasswordRoute
-  '/pool': typeof PoolRoute
-  '/trade': typeof TradeRoute
+  '/pool': typeof PoolRoute;
+  '/trade': typeof TradeRoute;
 }
 
 export interface FileRoutesById {
-  __root__: typeof rootRoute
-  '/': typeof IndexRoute
+  __root__: typeof rootRoute;
+  '/': typeof IndexRoute;
+  '/login': typeof LoginRoute;
   '/change-password': typeof ChangePasswordRoute
-  '/pool': typeof PoolRoute
-  '/trade': typeof TradeRoute
+  '/pool': typeof PoolRoute;
+  '/trade': typeof TradeRoute;
 }
 
 export interface FileRouteTypes {
-  fileRoutesByFullPath: FileRoutesByFullPath
-  fullPaths: '/' | '/change-password' | '/pool' | '/trade'
-  fileRoutesByTo: FileRoutesByTo
-  to: '/' | '/change-password' | '/pool' | '/trade'
-  id: '__root__' | '/' | '/change-password' | '/pool' | '/trade'
-  fileRoutesById: FileRoutesById
+  fileRoutesByFullPath: FileRoutesByFullPath;
+  fullPaths: '/' | '/login' | '/change-password | '/pool' | '/trade';
+  fileRoutesByTo: FileRoutesByTo;
+  to: '/' | '/login' | '/change-password | '/pool' | '/trade';
+  id: '__root__' | '/' | '/login' | '/change-password | '/pool' | '/trade';
+  fileRoutesById: FileRoutesById;
path: margin_frontend/src/routeTree.gen.ts
user: login: vadim-ghostman
body: there are missing quotation marks
created_at: 2025-03-24T21:29:38Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,135 @@
+import { useState } from 'react';
+
+import { Input } from '../../core/input';
+import { Card } from '../../core/card';
+import { Button } from '../../core/button';
+
+const textData = {
+  h1: 'Welcome back!',
path: margin_frontend/src/ui/views/LoginForm/LoginForm.tsx
user: login: vadim-ghostman
body: this field is not using, remove it or set as title
created_at: 2025-03-24T21:32:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,135 @@
+import { useState } from 'react';
+
+import { Input } from '../../core/input';
+import { Card } from '../../core/card';
+import { Button } from '../../core/button';
+
+const textData = {
+  h1: 'Welcome back!',
+  hint: 'Please enter your credentials to sign in!',
+  email: 'Email',
+  password: 'Password',
+  login: 'Sign In',
+  rememberMe: 'Remember Me',
+  forgotPassword: 'Forgot Password?',
+  singUp: 'Sign Up',
+  singUpHint: 'Don’t have an account yet?'
+};
+
+type LoginFormSchema = {
+  email: string;
+  password: string;
+  rememberMe: boolean;
+};
+
+const LoginForm = () => {
+  const [passwordVisibility, setPasswordVisibility] = useState(false);
+  const [email, setEmail] = useState('login@mail.com');
+  const [password, setPassword] = useState('password');
path: margin_frontend/src/ui/views/LoginForm/LoginForm.tsx
user: login: vadim-ghostman
body: I think email and password should be empty, but you can set some text in placeholders if you want
created_at: 2025-03-24T21:34:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,135 @@
+import { useState } from 'react';
+
+import { Input } from '../../core/input';
+import { Card } from '../../core/card';
+import { Button } from '../../core/button';
+
+const textData = {
+  h1: 'Welcome back!',
+  hint: 'Please enter your credentials to sign in!',
+  email: 'Email',
+  password: 'Password',
+  login: 'Sign In',
+  rememberMe: 'Remember Me',
+  forgotPassword: 'Forgot Password?',
+  singUp: 'Sign Up',
+  singUpHint: 'Don’t have an account yet?'
+};
+
+type LoginFormSchema = {
+  email: string;
+  password: string;
+  rememberMe: boolean;
+};
+
+const LoginForm = () => {
+  const [passwordVisibility, setPasswordVisibility] = useState(false);
+  const [email, setEmail] = useState('login@mail.com');
+  const [password, setPassword] = useState('password');
+  const [rememberMe, setRememberMe] = useState(false);
+
+  const onSendForm = async () => {
+    const data: LoginFormSchema = {
+      email,
+      password,
+      rememberMe
+    };
+
+    console.log(data);
+  };
+
+  return (
+    <Card className='text-white flex gap-6 flex-col font-bricolageGrotesque'>
+      <div className='flex flex-col gap-2'>
+        <h1 className='text-2xl font-bold text-center'>{textData.login}</h1>
+        <p>{textData.hint}</p>
path: margin_frontend/src/ui/views/LoginForm/LoginForm.tsx
user: login: vadim-ghostman
body: if the title is centered, then why is the hint not?
created_at: 2025-03-24T21:35:52Z
author_association: COLLABORATOR
path: margin_frontend/src/ui/views/LoginForm/index.ts
user: login: vadim-ghostman
body: this file can be removed, and `LoginForm.tsx` can be placed higher in the hierarchy (`src/ui/views`)
created_at: 2025-03-24T21:37:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,139 @@
+import { useState } from 'react';
+
+import { Input } from '../core/input';
+import { Card } from '../core/card';
+import { Button } from '../core/button';
+
+const textData = {
+  h1: 'Welcome back!',
+  hint: 'Please enter your credentials to sign in!',
+  email: 'Email',
+  emailPlaceholder: 'Enter your email',
+  password: 'Password',
+  passwordPlaceholder: 'Enter your password',
+  login: 'Sign In',
+  rememberMe: 'Remember Me',
+  forgotPassword: 'Forgot Password?',
+  singUp: 'Sign Up',
+  singUpHint: 'Don’t have an account yet?'
path: margin_frontend/src/ui/views/LoginForm.tsx
user: login: vadim-ghostman
body: `signUp`*
created_at: 2025-03-25T08:32:41Z
author_association: COLLABORATOR

PR comments:
user: login: kilinich-v
created_at: 2025-03-23T17:40:19Z
body: Do I understand correctly that I only need to add a component (login form)? Because in the [example](https://github.com/djeck1432/spotnet/pull/743), another rout is added.
author_association: CONTRIBUTOR
user: login: vadim-ghostman
created_at: 2025-03-23T20:13:27Z
body: it would be nice to add a route to have a separate endpoint for the login
author_association: COLLABORATOR
user: login: kilinich-v
created_at: 2025-03-23T20:48:49Z
body: fixed LoginForm and added route
author_association: CONTRIBUTOR
user: login: kilinich-v
created_at: 2025-03-24T22:23:22Z
body: done
author_association: CONTRIBUTOR
user: login: kilinich-v
created_at: 2025-03-25T11:25:18Z
body: Thanks, didn't notice it at first. fixed it
author_association: CONTRIBUTOR