Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
643 changes: 396 additions & 247 deletions web/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"firebase": "^12.9.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router": "^7.13.1"
"react-router": "^7.13.1",
"react-router-dom": "^7.13.1",
"styled-components": "^6.3.11"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
Expand Down
22 changes: 12 additions & 10 deletions web/src/App.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change the routing? Use createBrowserRouter. It's the newer and recommended way to do routing using react-router.

Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createBrowserRouter, RouterProvider } from "react-router";

const router = createBrowserRouter([{path: "/", element: <div>Hello World</div>}]);


export default function App() {


return <RouterProvider router={router}></RouterProvider>;
}
import { RouterProvider } from 'react-router';
import router from './routes/routes';
import { AuthProvider } from './context/AuthContext';
const App: React.FC = () => {
return (
<AuthProvider>
<RouterProvider router={router} />;
</AuthProvider>
);
};

export default App;
111 changes: 111 additions & 0 deletions web/src/components/atoms/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import styled, { css, keyframes } from 'styled-components';

const spin = keyframes`
to { transform: rotate(360deg); }
`;

type Variant = 'primary' | 'secondary' | 'google';

interface StyledButtonProps {
$variant: Variant;
$fullWidth?: boolean;
}

const StyledButton = styled.button<StyledButtonProps>`
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: ${({ $fullWidth }) => ($fullWidth ? '100%' : '173px')};
height: 42px;
padding: 12px 24px;
font-size: 15px;
font-weight: 400;
letter-spacing: 0px;
position: relative;
overflow: hidden;
border: none;
border-radius: 10px;

&:disabled {
opacity: 0.55;
cursor: not-allowed;
}
${({ $variant }) =>
$variant === 'primary' &&
css`
background-color: var(--color-primary);
color: var(--color-on-primary);
&:not(:disabled):hover {
transform: translateY(-0.5px);
}
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
`}

${({ $variant }) =>
$variant === 'secondary' &&
css`
background-color: var(--color-surface-bright);
color: var(--color-on-secondary);
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);

&:not(:disabled):hover {
transform: translateY(-0.5px);
}
`}

${({ $variant }) =>
$variant === 'google' &&
css`
background-color: var(--color-surface);
border: 1px solid var(--color-outline);
color: var(--color-on-surface);

&:not(:disabled):hover {
transform: translateY(-0.5px);
}
`}
`;

const Spinner = styled.span`
width: 16px;
height: 16px;
border: 2px solid var(--color-surface-bright);
border-top-color: var(--color-on-surface);
border-radius: 50%;
animation: ${spin} 0.7s linear infinite;
display: inline-block;
`;

interface ButtonProps {
variant?: Variant;
fullWidth?: boolean;
loading?: boolean;
disabled?: boolean;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
children: React.ReactNode;
}

export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
fullWidth = false,
loading = false,
disabled,
onClick,
type = 'button',
children,
}) => {
return (
<StyledButton
$variant={variant}
$fullWidth={fullWidth}
disabled={disabled || loading}
onClick={onClick}
type={type}
>
{loading ? <Spinner /> : children}
</StyledButton>
);
};
34 changes: 34 additions & 0 deletions web/src/components/atoms/Divider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`
display: flex;
align-items: center;
gap: 12px;
width: 100%;
`;

const Line = styled.div`
flex: 1;
height: 2px;
margin: 15px 0;
background-color: var(--color-on-surface-variant);
`;

const Label = styled.span`
font-size: 15px;
color: var(--color-on-surface-variant);
font-weight: 400;
`;

interface DividerProps {
label?: string;
}

export const Divider: React.FC<DividerProps> = ({ label = 'Or' }) => (
<Wrapper>
<Line />
<Label>{label}</Label>
<Line />
</Wrapper>
);
19 changes: 19 additions & 0 deletions web/src/components/atoms/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import styled, { keyframes } from 'styled-components';

const slideIn = keyframes`
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
`;

const Wrapper = styled.span`
font-size: 0.75rem;
color: var(--color-primary);
animation: ${slideIn} 0.2s ease;
line-height: 0.2;
text-align: center;
`;

export const ErrorMessage: React.FC<{ message: string }> = ({ message }) => (
<Wrapper role="alert">{message}</Wrapper>
);
47 changes: 47 additions & 0 deletions web/src/components/atoms/GoogleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { Button } from './Button';

interface GoogleButtonProps {
onClick: () => void;
loading?: boolean;
}

const GoogleIcon = () => (
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
xmlnsXlink="http://www.w3.org/1999/xlink"
style={{ display: 'block' }}
width={18}
height={18}
>
<path
fill="#EA4335"
d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
></path>
<path
fill="#4285F4"
d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
></path>
<path
fill="#FBBC05"
d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"
></path>
<path
fill="#34A853"
d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
></path>
<path fill="none" d="M0 0h48v48H0z"></path>
</svg>
);

export const GoogleButton: React.FC<GoogleButtonProps> = ({
onClick,
loading,
}) => (
<Button variant="google" fullWidth loading={loading} onClick={onClick}>
<GoogleIcon />
Continue with Google
</Button>
);
47 changes: 47 additions & 0 deletions web/src/components/atoms/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`
position: relative;
width: 100%;
display: flex;
justify-content: center;
`;

const StyledInput = styled.input`
width: 100%;
max-width: 273px;
height: 42px;
padding: 21px 11px;
font-size: 15px;
font-weight: 400;
background-color:var(--color-surface-container);
border:none;
placeholder-color:rgba(75, 75, 75, 1);
color:var(--color-on-surface);
outline:none;
&:focus {
outline: 1px solid var(--color-primary);
}
border-radius:10px;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
error?: boolean;
}

export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ error: _error, ...rest }, ref) => {
return (
<Wrapper>
<StyledInput ref={ref} {...rest} />
</Wrapper>
);
},
);

Input.displayName = 'Input';
48 changes: 48 additions & 0 deletions web/src/components/atoms/Logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
@media (max-width: 393px) {
gap: 6px;
}
`;
const LogoImage = styled.img`
margin-bottom: 30px;
@media (max-width: 393px) {
margin-bottom: 15px;
}
`;

const Title = styled.h1`
font-size: 2.4rem;
letter-spacing: 0.04em;
line-height: 1;
`;

const Subtitle = styled.p`
font-size: 20px;
letter-spacing: 0.02em;
`;

interface LogoProps {
subtitle?: string;
}

export const Logo: React.FC<LogoProps> = ({
subtitle = 'Your key to riches.',
}) => {
return (
<Wrapper>
<LogoImage src="./logo.webp" alt="TwoAxis Finance Logo" />

<div style={{ textAlign: 'center' }}>
<Title>TwoAxis Finance</Title>
<Subtitle>{subtitle}</Subtitle>
</div>
</Wrapper>
);
};
25 changes: 25 additions & 0 deletions web/src/components/atoms/PrivacyNote.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { Link } from 'react-router';
import styled from 'styled-components';

const Text = styled.p`
font-size: 15px;

text-align: center;
line-height: 100%;
font-weight: 400;
color: var(--color-on-surface-variant);
a {
text-decoration: underline;
color: var(--color-on-surface-variant);
}
`;

export const PrivacyNote: React.FC = () => (
<Text>
By continuing, you agree to our{' '}
<Link to="https://finance.twoaxis.org/privacy" target="_blank" rel="noopener noreferrer">
Privacy Policy
</Link>
</Text>
);
Loading