From 2984b3ae93794a61ff2e0daaaeff2bee8278c6db Mon Sep 17 00:00:00 2001 From: Mariam Hagras Date: Sun, 1 Mar 2026 21:30:45 +0200 Subject: [PATCH 1/7] feat(auth):creating auth pages --- web/package-lock.json | 207 ++++++++++++++++++++++++++++- web/package.json | 6 +- web/src/App.tsx | 48 ++++++- web/src/atoms/Button.tsx | 136 +++++++++++++++++++ web/src/atoms/Divider.tsx | 34 +++++ web/src/atoms/ErrorMessage.tsx | 18 +++ web/src/atoms/Input.tsx | 50 +++++++ web/src/atoms/LoadingScreen.tsx | 30 +++++ web/src/atoms/Logo.tsx | 54 ++++++++ web/src/atoms/PrivacyNote.tsx | 28 ++++ web/src/context/AuthContext.tsx | 109 +++++++++++++++ web/src/guards/RouteGuards.tsx | 26 ++++ web/src/index.css | 26 ---- web/src/lib/firebase/firebase.ts | 3 +- web/src/main.tsx | 1 - web/src/molecules/GoogleButton.tsx | 35 +++++ web/src/molecules/LoginForm.tsx | 99 ++++++++++++++ web/src/molecules/RegisterForm.tsx | 103 ++++++++++++++ web/src/organisms/LandingCard.tsx | 50 +++++++ web/src/organisms/LoginCard.tsx | 22 +++ web/src/organisms/RegisterCard.tsx | 23 ++++ web/src/pages/AuthPage.tsx | 24 ++++ web/src/pages/DashboardPage.tsx | 54 ++++++++ web/src/routes/AppRouter.tsx | 0 web/src/styled.d.ts | 6 + web/src/styles/theme.ts | 76 +++++++++++ web/src/templates/AuthLayout.tsx | 80 +++++++++++ web/src/types/index.ts | 20 +++ 28 files changed, 1328 insertions(+), 40 deletions(-) create mode 100644 web/src/atoms/Button.tsx create mode 100644 web/src/atoms/Divider.tsx create mode 100644 web/src/atoms/ErrorMessage.tsx create mode 100644 web/src/atoms/Input.tsx create mode 100644 web/src/atoms/LoadingScreen.tsx create mode 100644 web/src/atoms/Logo.tsx create mode 100644 web/src/atoms/PrivacyNote.tsx create mode 100644 web/src/context/AuthContext.tsx create mode 100644 web/src/guards/RouteGuards.tsx delete mode 100644 web/src/index.css create mode 100644 web/src/molecules/GoogleButton.tsx create mode 100644 web/src/molecules/LoginForm.tsx create mode 100644 web/src/molecules/RegisterForm.tsx create mode 100644 web/src/organisms/LandingCard.tsx create mode 100644 web/src/organisms/LoginCard.tsx create mode 100644 web/src/organisms/RegisterCard.tsx create mode 100644 web/src/pages/AuthPage.tsx create mode 100644 web/src/pages/DashboardPage.tsx create mode 100644 web/src/routes/AppRouter.tsx create mode 100644 web/src/styled.d.ts create mode 100644 web/src/styles/theme.ts create mode 100644 web/src/templates/AuthLayout.tsx create mode 100644 web/src/types/index.ts diff --git a/web/package-lock.json b/web/package-lock.json index 0ab129c..34b15ad 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,10 +9,14 @@ "version": "0.0.0", "dependencies": { "@fontsource/sen": "^5.2.8", + "@hookform/error-message": "^2.0.1", + "@hookform/resolvers": "^5.2.2", "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", @@ -311,6 +315,27 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -1558,6 +1583,29 @@ "node": ">=6" } }, + "node_modules/@hookform/error-message": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.1.tgz", + "integrity": "sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2081,6 +2129,12 @@ "win32" ] }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2169,6 +2223,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/stylis": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz", + "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", @@ -2631,6 +2691,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001774", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", @@ -2743,11 +2812,30 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -3509,7 +3597,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -3625,7 +3712,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3670,6 +3756,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3735,6 +3827,23 @@ "react": "^19.2.4" } }, + "node_modules/react-hook-form": { + "version": "7.71.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", + "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3767,6 +3876,22 @@ } } }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3873,6 +3998,12 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3900,7 +4031,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -3945,6 +4075,73 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.3.11", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.11.tgz", + "integrity": "sha512-opzgceGlQ5rdZdGwf9ddLW7EM2F4L7tgsgLn6fFzQ2JgE5EVQ4HZwNkcgB1p8WfOBx1GEZP3fa66ajJmtXhSrA==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.4.0", + "@emotion/unitless": "0.10.0", + "@types/stylis": "4.2.7", + "css-to-react-native": "3.2.0", + "csstype": "3.2.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.6", + "tslib": "2.8.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/web/package.json b/web/package.json index 6784127..4b19ee7 100644 --- a/web/package.json +++ b/web/package.json @@ -11,10 +11,14 @@ }, "dependencies": { "@fontsource/sen": "^5.2.8", + "@hookform/error-message": "^2.0.1", + "@hookform/resolvers": "^5.2.2", "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", diff --git a/web/src/App.tsx b/web/src/App.tsx index 996f4bf..48ef811 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,10 +1,46 @@ -import { createBrowserRouter, RouterProvider } from "react-router"; +import React from 'react' +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { ThemeProvider } from 'styled-components' +import { AuthProvider } from './context/AuthContext' +import { ProtectedRoute, PublicRoute } from './guards/RouteGuards' +import { AuthPage } from './pages/AuthPage' +import { DashboardPage } from './pages/DashboardPage' +import { GlobalStyles, theme } from './styles/theme' -const router = createBrowserRouter([{path: "/", element:
Hello World
}]); +const App: React.FC = () => { + return ( + + + + + + {/* Public: redirect to dashboard if already logged in */} + + + + } + /> + {/* Protected: redirect to /auth if not logged in */} + + + + } + /> -export default function App() { - + {/* Default redirect */} + } /> + + + + + ) +} - return ; -} \ No newline at end of file +export default App diff --git a/web/src/atoms/Button.tsx b/web/src/atoms/Button.tsx new file mode 100644 index 0000000..264bcae --- /dev/null +++ b/web/src/atoms/Button.tsx @@ -0,0 +1,136 @@ +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` + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: ${({ $fullWidth }) => ($fullWidth ? '100%' : '173px')}; + padding: 12px 24px; + border-radius: ${({ theme }) => theme.radii.md}; + font-size: 0.875rem; + font-weight: 500; + letter-spacing: 0.02em; + transition: all ${({ theme }) => theme.transitions.normal}; + position: relative; + overflow: hidden; + + &:disabled { + opacity: 0.55; + cursor: not-allowed; + } + + &::after { + content: ''; + position: absolute; + inset: 0; + background: white; + opacity: 0; + transition: opacity ${({ theme }) => theme.transitions.fast}; + } + + &:not(:disabled):hover::after { + opacity: 0.06; + } + + &:not(:disabled):active::after { + opacity: 0.1; + } + + ${({ $variant, theme }) => + $variant === 'primary' && + css` + background: ${theme.colors.red}; + color: ${theme.colors.white}; + box-shadow: 0 4px 20px hsla(6, 63%, 46%, 0.35); + + &:not(:disabled):hover { + background: ${theme.colors.redHover}; + box-shadow: 0 8px 20px rgba(94, 25, 25, 0.35); + + transform: translateY(-1px); + } + `} + + ${({ $variant, theme }) => + $variant === 'secondary' && + css` + background: ${theme.colors.bgInput}; + color: ${theme.colors.text}; + border: 1px solid ${theme.colors.border}; + + &:not(:disabled):hover { + border-color: ${theme.colors.muted}; + transform: translateY(-1px); + } + `} + + ${({ $variant, theme }) => + $variant === 'google' && + css` + background: ${theme.colors.google}; + color: ${theme.colors.text}; + border: 1px solid ${theme.colors.border}; + + &:not(:disabled):hover { + border-color: ${theme.colors.muted}; + transform: translateY(-1px); + } + `} +` + +const Spinner = styled.span` + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + 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 = ({ + variant = 'primary', + fullWidth = false, + loading = false, + disabled, + onClick, + type = 'button', + children, +}) => { + return ( + + {loading ? : children} + + ) +} diff --git a/web/src/atoms/Divider.tsx b/web/src/atoms/Divider.tsx new file mode 100644 index 0000000..dcb15d6 --- /dev/null +++ b/web/src/atoms/Divider.tsx @@ -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: ${({ theme }) => theme.colors.overmuted}; +` + +const Label = styled.span` + font-size: 15px; + color: ${({ theme }) => theme.colors.white}; + font-weight: 400; +` + +interface DividerProps { + label?: string +} + +export const Divider: React.FC = ({ label = 'Or' }) => ( + + + + + +) diff --git a/web/src/atoms/ErrorMessage.tsx b/web/src/atoms/ErrorMessage.tsx new file mode 100644 index 0000000..a050d07 --- /dev/null +++ b/web/src/atoms/ErrorMessage.tsx @@ -0,0 +1,18 @@ +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: #e57373; + animation: ${slideIn} 0.2s ease; + line-height: 0.5; +` + +export const ErrorMessage: React.FC<{ message: string }> = ({ message }) => ( + {message} +) diff --git a/web/src/atoms/Input.tsx b/web/src/atoms/Input.tsx new file mode 100644 index 0000000..321a0b0 --- /dev/null +++ b/web/src/atoms/Input.tsx @@ -0,0 +1,50 @@ +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; + background: ${({ theme }) => theme.colors.bgInput}; + border: 1px solid ${({ theme }) => theme.colors.border}; + border-radius: ${({ theme }) => theme.radii.md}; + padding: 14px 16px; + font-size: 0.9rem; + color: ${({ theme }) => theme.colors.text}; + transition: border-color ${({ theme }) => theme.transitions.normal}, + box-shadow ${({ theme }) => theme.transitions.normal}; + + &::placeholder { + color: ${({ theme }) => theme.colors.muted}; + } + + + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +` + +interface InputProps extends React.InputHTMLAttributes { + error?: boolean +} + +export const Input = React.forwardRef( + ({ error: _error, ...rest }, ref) => { + return ( + + + + ) + } +) + +Input.displayName = 'Input' diff --git a/web/src/atoms/LoadingScreen.tsx b/web/src/atoms/LoadingScreen.tsx new file mode 100644 index 0000000..515da5d --- /dev/null +++ b/web/src/atoms/LoadingScreen.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import styled, { keyframes } from 'styled-components' + +const spin = keyframes` + to { transform: rotate(360deg); } +` + +const Wrapper = styled.div` + height: 100vh; + width: 100vw; + background: ${({ theme }) => theme.colors.bg}; + display: flex; + align-items: center; + justify-content: center; +` + +const Ring = styled.div` + width: 40px; + height: 40px; + border: 3px solid ${({ theme }) => theme.colors.border}; + border-top-color: ${({ theme }) => theme.colors.red}; + border-radius: 50%; + animation: ${spin} 0.8s linear infinite; +` + +export const LoadingScreen: React.FC = () => ( + + + +) diff --git a/web/src/atoms/Logo.tsx b/web/src/atoms/Logo.tsx new file mode 100644 index 0000000..bfc8708 --- /dev/null +++ b/web/src/atoms/Logo.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import styled from 'styled-components'; + +; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + + +`; +const LogoImage = styled.img` + margin-bottom:30px; + + `; + + + + +const Title = styled.h1` + font-family: ${({ theme }) => theme.fonts.display}; + font-size: 2.4rem; + letter-spacing: 0.04em; + color: ${({ theme }) => theme.colors.white}; + line-height: 1; +`; + +const Subtitle = styled.p` + font-size:20px; + color: ${({ theme }) => theme.colors.textSub}; + font-weight: 300; + letter-spacing: 0.02em; +`; + +interface LogoProps { + subtitle?: string; +} + +export const Logo: React.FC = ({ + subtitle = 'Your key to riches.', +}) => { + return ( + + + +
+ TwoAxis Finance + {subtitle} +
+
+ ); +}; diff --git a/web/src/atoms/PrivacyNote.tsx b/web/src/atoms/PrivacyNote.tsx new file mode 100644 index 0000000..3ec23a3 --- /dev/null +++ b/web/src/atoms/PrivacyNote.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import styled from 'styled-components' + +const Text = styled.p` + font-size: 15px; + color: ${({ theme }) => theme.colors.overmuted}; + text-align: center; + line-height: 1.5; + + a { + color: ${({ theme }) => theme.colors.overmuted}; + text-decoration: underline; + text-underline-offset: 2px; + + &:hover { + color: ${({ theme }) => theme.colors.white}; + } + } +` + +export const PrivacyNote: React.FC = () => ( + + By continuing, you agree to our{' '} + + Privacy Policy + + +) diff --git a/web/src/context/AuthContext.tsx b/web/src/context/AuthContext.tsx new file mode 100644 index 0000000..e88b688 --- /dev/null +++ b/web/src/context/AuthContext.tsx @@ -0,0 +1,109 @@ +import React, { createContext, useContext, useEffect, useState, useCallback } from 'react' +import { + onAuthStateChanged, + signInWithEmailAndPassword, + createUserWithEmailAndPassword, + signInWithPopup, + signOut, + type FirebaseError, +} from 'firebase/auth' +import { auth, googleProvider } from '../lib/firebase/firebase' +import type { User, AuthContextType } from '../types' + +const AuthContext = createContext(null) + +function mapFirebaseError(error: FirebaseError): string { + switch (error.code) { + case 'auth/user-not-found': + case 'auth/wrong-password': + case 'auth/invalid-credential': + return 'Invalid email or password.' + case 'auth/email-already-in-use': + return 'This email is already registered.' + case 'auth/weak-password': + return 'Password must be at least 6 characters.' + case 'auth/invalid-email': + return 'Please enter a valid email address.' + case 'auth/too-many-requests': + return 'Too many attempts. Please try again later.' + case 'auth/popup-closed-by-user': + return 'Google sign-in was cancelled.' + default: + return 'An unexpected error occurred. Please try again.' + } +} + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => { + if (firebaseUser) { + setUser({ + uid: firebaseUser.uid, + email: firebaseUser.email, + displayName: firebaseUser.displayName, + photoURL: firebaseUser.photoURL, + emailVerified: firebaseUser.emailVerified, + }) + } else { + setUser(null) + } + setLoading(false) + }) + + return () => unsubscribe() + }, []) + + const clearError = useCallback(() => setError(null), []) + + const loginWithEmail = useCallback(async (email: string, password: string) => { + try { + setError(null) + await signInWithEmailAndPassword(auth, email, password) + } catch (err) { + setError(mapFirebaseError(err as FirebaseError)) + throw err + } + }, []) + + const registerWithEmail = useCallback(async (email: string, password: string) => { + try { + setError(null) + await createUserWithEmailAndPassword(auth, email, password) + } catch (err) { + setError(mapFirebaseError(err as FirebaseError)) + throw err + } + }, []) + + const loginWithGoogle = useCallback(async () => { + try { + setError(null) + await signInWithPopup(auth, googleProvider) + } catch (err) { + setError(mapFirebaseError(err as FirebaseError)) + throw err + } + }, []) + + const logout = useCallback(async () => { + await signOut(auth) + }, []) + + return ( + + {children} + + ) +} + +export const useAuth = (): AuthContextType => { + const ctx = useContext(AuthContext) + if (!ctx) throw new Error('useAuth must be used inside AuthProvider') + return ctx +} diff --git a/web/src/guards/RouteGuards.tsx b/web/src/guards/RouteGuards.tsx new file mode 100644 index 0000000..8bdd17e --- /dev/null +++ b/web/src/guards/RouteGuards.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { Navigate } from 'react-router-dom' +import { useAuth } from '../context/AuthContext' +import { LoadingScreen } from '../atoms/LoadingScreen' + +// ─── Protected Route ───────────────────────────────────────── +// Redirects unauthenticated users to /auth +export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { user, loading } = useAuth() + + if (loading) return + if (!user) return + + return <>{children} +} + +// ─── Public Route ───────────────────────────────────────────── +// Redirects authenticated users away from auth pages to /dashboard +export const PublicRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { user, loading } = useAuth() + + if (loading) return + if (user) return + + return <>{children} +} diff --git a/web/src/index.css b/web/src/index.css deleted file mode 100644 index 7de77b3..0000000 --- a/web/src/index.css +++ /dev/null @@ -1,26 +0,0 @@ -:root { - --color-primary: rgb(167, 34, 34); - --color-primary-container: rgb(167, 34, 34); - --color-on-primary: white; - --color-secondary: rgb(94, 25, 25); - --color-secondary-container: rgb(94, 25, 25); - --color-on-secondary: white; - --color-surface: rgb(20, 20, 20); - --color-surface-container: rgb(30, 30, 30); - --color-surface-bright: rgb(40, 40, 40); - --color-on-surface: white; - --color-on-surface-variant: rgb(100, 100, 100); - --color-outline: rgb(100, 100, 100); -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; - font-family: "Sen", sans-serif; -} - -body { - background-color: var(--color-surface); - color: var(--color-on-surface); -} \ No newline at end of file diff --git a/web/src/lib/firebase/firebase.ts b/web/src/lib/firebase/firebase.ts index 8a935bc..de8bee0 100644 --- a/web/src/lib/firebase/firebase.ts +++ b/web/src/lib/firebase/firebase.ts @@ -2,7 +2,7 @@ import { initializeApp } from "firebase/app"; import { getAnalytics } from "firebase/analytics"; import { connectAuthEmulator, getAuth } from "firebase/auth"; import { connectFirestoreEmulator, getFirestore } from "firebase/firestore"; - +import { GoogleAuthProvider } from "firebase/auth"; const firebaseConfig = { apiKey: "AIzaSyDhXWp5E7LI2pUY61aJX2jDWlwoZUOQ_6Q", authDomain: "financial-planner-72109.firebaseapp.com", @@ -17,6 +17,7 @@ export const app = initializeApp(firebaseConfig); export const auth = getAuth(app); export const db = getFirestore(app); export const analytics = getAnalytics(app); +export const googleProvider = new GoogleAuthProvider(); if(window.location.hostname === "localhost") { connectAuthEmulator(auth, "http://localhost:9099"); diff --git a/web/src/main.tsx b/web/src/main.tsx index 5f14872..3e1e2c4 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -2,7 +2,6 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import "@fontsource/sen/400.css"; import "@fontsource/sen/800.css"; -import './index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( diff --git a/web/src/molecules/GoogleButton.tsx b/web/src/molecules/GoogleButton.tsx new file mode 100644 index 0000000..46f0852 --- /dev/null +++ b/web/src/molecules/GoogleButton.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { Button } from '../atoms/Button' + +const GoogleIcon: React.FC = () => ( + + + + + + +) + +interface GoogleButtonProps { + onClick: () => void + loading?: boolean +} + +export const GoogleButton: React.FC = ({ onClick, loading }) => ( + +) diff --git a/web/src/molecules/LoginForm.tsx b/web/src/molecules/LoginForm.tsx new file mode 100644 index 0000000..0102a8f --- /dev/null +++ b/web/src/molecules/LoginForm.tsx @@ -0,0 +1,99 @@ +import React, { useRef } from 'react' +import styled from 'styled-components' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' +import { Input } from '../atoms/Input' +import { Button } from '../atoms/Button' +import { ErrorMessage } from '../atoms/ErrorMessage' +import { useAuth } from '../context/AuthContext' + +const schema = z.object({ + email: z.email('Enter a valid email'), + password: z.string().min(1, 'Password is required') +}) + +type LoginFields = z.infer + +const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + width: 100%; + & > :last-child { + margin-top: 20px; + } +` + +interface LoginFormProps { + onSuccess?: () => void +} + +export const LoginForm: React.FC = ({ onSuccess }) => { + const { loginWithEmail, error, clearError } = useAuth() + + // Track whether user has attempted a failed submit + const hasFailedSubmit = useRef(false) + + const { + register, + handleSubmit, + formState: { errors, isSubmitting, isValid }, + } = useForm({ + resolver: zodResolver(schema), + mode: 'onChange', // only active after a failed submit + }) + + // Re-validate live only after user hit submit with bad data + // so the button can unlock as soon as fields are corrected + const isLockedOut = hasFailedSubmit.current && !isValid + + const onSubmit = async (data: LoginFields) => { + clearError() + try { + await loginWithEmail(data.email, data.password) + onSuccess?.() + } catch (err) { + // error handled by context + console.error('Login failed', err) + } + } + + const onError = () => { + // User tried to submit invalid data — now we lock until fixed + hasFailedSubmit.current = true + } + + return ( +
+ {error && } + + + {errors.email && } + + + {errors.password && } + + + + ) +} \ No newline at end of file diff --git a/web/src/molecules/RegisterForm.tsx b/web/src/molecules/RegisterForm.tsx new file mode 100644 index 0000000..e17c209 --- /dev/null +++ b/web/src/molecules/RegisterForm.tsx @@ -0,0 +1,103 @@ +import React, { useRef } from 'react' +import styled from 'styled-components' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' +import { Input } from '../atoms/Input' +import { Button } from '../atoms/Button' +import { ErrorMessage } from '../atoms/ErrorMessage' +import { useAuth } from '../context/AuthContext' + +const schema = z + .object({ + email: z.email('Enter a valid email'), + password: z.string().min(8, 'Password must be at least 8 characters'), + repeatPassword: z.string().min(1, 'Please confirm your password'), + }) + .refine((data) => data.password === data.repeatPassword, { + message: 'Passwords do not match', + path: ['repeatPassword'], // attach error to repeatPassword field + }) + +type RegisterFields = z.infer + +const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + width: 100%; + & > :last-child { + margin-top: 16px; + } +` + +export const RegisterForm: React.FC = () => { + const { registerWithEmail, error, clearError } = useAuth() + const hasFailedSubmit = useRef(false) + + const { + register, + handleSubmit, + formState: { errors, isSubmitting, isValid }, + } = useForm({ + resolver: zodResolver(schema), + mode: 'onChange', + }) + + const isLockedOut = hasFailedSubmit.current && !isValid + + const onSubmit = async (data: RegisterFields) => { + clearError() + try { + await registerWithEmail(data.email, data.password) + } catch(err) { + console.error('Registration failed', err); + // handled by context + } + } + + const onError = () => { + hasFailedSubmit.current = true + } + + return ( +
+ {error && } + + + {errors.email && } + + + {errors.password && } + + + {errors.repeatPassword && } + + + + ) +} \ No newline at end of file diff --git a/web/src/organisms/LandingCard.tsx b/web/src/organisms/LandingCard.tsx new file mode 100644 index 0000000..d44150f --- /dev/null +++ b/web/src/organisms/LandingCard.tsx @@ -0,0 +1,50 @@ +import React, { useState } from 'react' +import styled from 'styled-components' +import { Button } from '../atoms/Button' +import { Divider } from '../atoms/Divider' +import { GoogleButton } from '../molecules/GoogleButton' +import { useAuth } from '../context/AuthContext' +import { ErrorMessage } from '../atoms/ErrorMessage' +import type { AuthView } from '../types' + +const Actions = styled.div` + display: flex; + flex-direction: column; + gap: 10px; + +` + +interface LandingCardProps { + onViewChange: (view: AuthView) => void +} + +export const LandingCard: React.FC = ({ onViewChange }) => { + const { loginWithGoogle, error, clearError } = useAuth() + const [googleLoading, setGoogleLoading] = useState(false) + + const handleGoogle = async () => { + try { + setGoogleLoading(true) + clearError() + await loginWithGoogle() + } catch { + // handled by context + } finally { + setGoogleLoading(false) + } + } + + return ( + + {error && } + + + + + + ) +} diff --git a/web/src/organisms/LoginCard.tsx b/web/src/organisms/LoginCard.tsx new file mode 100644 index 0000000..8fb13eb --- /dev/null +++ b/web/src/organisms/LoginCard.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import styled from 'styled-components' +import { LoginForm } from '../molecules/LoginForm' +import type { AuthView } from '../types' + + + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; +` + +interface LoginCardProps { + onViewChange: (view: AuthView) => void +} + +export const LoginCard: React.FC = ({ onViewChange }) => ( + + + +) diff --git a/web/src/organisms/RegisterCard.tsx b/web/src/organisms/RegisterCard.tsx new file mode 100644 index 0000000..76a3834 --- /dev/null +++ b/web/src/organisms/RegisterCard.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import styled from 'styled-components' +import { RegisterForm } from '../molecules/RegisterForm' +import type { AuthView } from '../types' + + + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +` + +interface RegisterCardProps { + onViewChange: (view: AuthView) => void +} + +export const RegisterCard: React.FC = ({ onViewChange }) => ( + + + +) diff --git a/web/src/pages/AuthPage.tsx b/web/src/pages/AuthPage.tsx new file mode 100644 index 0000000..9d98077 --- /dev/null +++ b/web/src/pages/AuthPage.tsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react' +import { AuthLayout } from '../templates/AuthLayout' +import { LandingCard } from '../organisms/LandingCard' +import { LoginCard } from '../organisms/LoginCard' +import { RegisterCard } from '../organisms/RegisterCard' +import type { AuthView } from '../types' + +const subtitleMap: Record = { + landing: 'Your key to riches.', + login: 'Login to your account', + register: 'Create an Account', +} + +export const AuthPage: React.FC = () => { + const [view, setView] = useState('landing') + + return ( + + {view === 'landing' && } + {view === 'login' && } + {view === 'register' && } + + ) +} diff --git a/web/src/pages/DashboardPage.tsx b/web/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..7b1e8b7 --- /dev/null +++ b/web/src/pages/DashboardPage.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import styled from 'styled-components' +import { useAuth } from '../context/AuthContext' +import { Button } from '../atoms/Button' + +const Page = styled.div` + min-height: 100vh; + background: ${({ theme }) => theme.colors.bg}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 24px; + padding: 24px; +` + +const Title = styled.h1` + font-family: ${({ theme }) => theme.fonts.display}; + font-size: 3rem; + color: ${({ theme }) => theme.colors.white}; + letter-spacing: 0.04em; +` + +const Subtitle = styled.p` + color: ${({ theme }) => theme.colors.textSub}; + font-size: 1rem; +` + +const Badge = styled.span` + background: rgba(192, 57, 43, 0.15); + border: 1px solid rgba(192, 57, 43, 0.3); + color: #e57373; + border-radius: ${({ theme }) => theme.radii.full}; + padding: 4px 14px; + font-size: 0.8rem; + font-weight: 500; +` + +export const DashboardPage: React.FC = () => { + const { user, logout } = useAuth() + + return ( + + ✓ Authenticated + TwoAxis Finance + + Welcome, {user?.email ?? 'User'} + + + + ) +} diff --git a/web/src/routes/AppRouter.tsx b/web/src/routes/AppRouter.tsx new file mode 100644 index 0000000..e69de29 diff --git a/web/src/styled.d.ts b/web/src/styled.d.ts new file mode 100644 index 0000000..c20cade --- /dev/null +++ b/web/src/styled.d.ts @@ -0,0 +1,6 @@ +import 'styled-components' +import type { Theme } from './styles/theme' + +declare module 'styled-components' { + export interface DefaultTheme extends Theme {} +} diff --git a/web/src/styles/theme.ts b/web/src/styles/theme.ts new file mode 100644 index 0000000..fb6bcc7 --- /dev/null +++ b/web/src/styles/theme.ts @@ -0,0 +1,76 @@ +import { createGlobalStyle } from 'styled-components' + +export const theme = { + colors: { + bg: '#111111', + bgCard: '#1a1a1a', + bgInput: '#1E1E1E', + red: '#A72222', + redHover: 'rgb(94, 25, 25)', + + white: '#ffffff', + border: '#2a2a2a', + muted: '#646464', + overmuted:'#626262', + borderFocus: '#c0392b', + text: '#e8e8e8', + google: '#141414', + }, + fonts: { + display: "'Sen', sans-serif", + body: "'Sen', sans-serif", + }, + radii: { + sm: '6px', + md: '10px', + lg: '14px', + full: '999px', + }, + transitions: { + fast: '0.15s ease', + normal: '0.25s ease', + }, +} + +export type Theme = typeof theme + +export const GlobalStyles = createGlobalStyle<{ theme: Theme }>` + *, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + html, body, #root { + height: 100%; + width: 100%; + } + + body { + font-family: ${({ theme }) => theme.fonts.body}; + background: ${({ theme }) => theme.colors.bg}; + color: ${({ theme }) => theme.colors.text}; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + a { + color: inherit; + text-decoration: none; + } + + button { + font-family: inherit; + cursor: pointer; + border: none; + outline: none; + background: none; + } + + input { + font-family: inherit; + outline: none; + border: none; + background: none; + } +` diff --git a/web/src/templates/AuthLayout.tsx b/web/src/templates/AuthLayout.tsx new file mode 100644 index 0000000..c949205 --- /dev/null +++ b/web/src/templates/AuthLayout.tsx @@ -0,0 +1,80 @@ +import React from 'react' +import styled, { keyframes } from 'styled-components' +import { Logo } from '../atoms/Logo' +import { PrivacyNote } from '../atoms/PrivacyNote' + +const fadeIn = keyframes` + from { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); } +` + +const Page = styled.div` + min-height: 100vh; + background: ${({ theme }) => theme.colors.bg}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 24px; + padding-top: 114px; + + position: relative; + overflow: hidden; + + /* Subtle background glow */ + &::before { + content: ''; + position: absolute; + top: -20%; + left: 50%; + transform: translateX(-50%); + width: 600px; + height: 600px; + background: radial-gradient(circle, rgba(192, 57, 43, 0.06) 0%, transparent 70%); + pointer-events: none; + } +` + +const Card = styled.div` + width: 100%; + max-width: 360px; + display: flex; + flex-direction: column; + align-items: center; + gap: 32px; + animation: ${fadeIn} 0.4s ease; +` + +const ContentArea = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + +` + +const Footer = styled.div` + display: flex; + justify-content: center; + bottom: 70px; + position: absolute; + min-width:360px; +` + +interface AuthLayoutProps { + subtitle?: string + children: React.ReactNode +} + +export const AuthLayout: React.FC = ({ subtitle, children }) => ( + + + + {children} +
+ +
+
+
+) diff --git a/web/src/types/index.ts b/web/src/types/index.ts new file mode 100644 index 0000000..4b2791c --- /dev/null +++ b/web/src/types/index.ts @@ -0,0 +1,20 @@ +export interface User { + uid: string + email: string | null + displayName: string | null + photoURL: string | null + emailVerified: boolean +} + +export interface AuthContextType { + user: User | null + loading: boolean + error: string | null + loginWithEmail: (email: string, password: string) => Promise + registerWithEmail: (email: string, password: string) => Promise + loginWithGoogle: () => Promise + logout: () => Promise + clearError: () => void +} + +export type AuthView = 'landing' | 'login' | 'register' From c929c874e2da54fa74e0579953f81548f594c647 Mon Sep 17 00:00:00 2001 From: Mariam Hagras Date: Sat, 7 Mar 2026 05:42:37 +0200 Subject: [PATCH 2/7] removing react-hook-form and using index.css --- web/package-lock.json | 588 ++++++++---------- web/package.json | 2 - web/src/App.tsx | 104 ++-- web/src/atoms/Button.tsx | 136 ---- web/src/atoms/Divider.tsx | 34 - web/src/atoms/ErrorMessage.tsx | 18 - web/src/atoms/Input.tsx | 50 -- web/src/atoms/LoadingScreen.tsx | 30 - web/src/atoms/PrivacyNote.tsx | 28 - web/src/components/atoms/Button.tsx | 111 ++++ web/src/components/atoms/Divider.tsx | 34 + web/src/components/atoms/ErrorMessage.tsx | 19 + web/src/components/atoms/GoogleButton.tsx | 43 ++ web/src/components/atoms/Input.tsx | 47 ++ web/src/{ => components}/atoms/Logo.tsx | 4 - web/src/components/atoms/PrivacyNote.tsx | 24 + web/src/components/molecules/LoginForm.tsx | 148 +++++ web/src/components/molecules/RegisterForm.tsx | 180 ++++++ .../organisms/LandingCard.tsx | 18 +- .../{ => components}/organisms/LoginCard.tsx | 9 +- .../organisms/RegisterCard.tsx | 9 +- web/src/components/pages/AuthPage.tsx | 4 + .../{ => components}/pages/DashboardPage.tsx | 11 +- .../{ => components}/templates/AuthLayout.tsx | 35 +- web/src/context/AuthContext.tsx | 2 +- web/src/guards/RouteGuards.tsx | 15 +- web/src/index.css | 29 + web/src/main.tsx | 1 + web/src/molecules/GoogleButton.tsx | 35 -- web/src/molecules/LoginForm.tsx | 99 --- web/src/molecules/RegisterForm.tsx | 103 --- web/src/pages/AuthPage.tsx | 24 - web/src/routes/authRoutes.ts | 16 + web/src/styled.d.ts | 6 - web/src/styles/theme.ts | 76 --- web/src/types/index.ts | 2 - 36 files changed, 1030 insertions(+), 1064 deletions(-) delete mode 100644 web/src/atoms/Button.tsx delete mode 100644 web/src/atoms/Divider.tsx delete mode 100644 web/src/atoms/ErrorMessage.tsx delete mode 100644 web/src/atoms/Input.tsx delete mode 100644 web/src/atoms/LoadingScreen.tsx delete mode 100644 web/src/atoms/PrivacyNote.tsx create mode 100644 web/src/components/atoms/Button.tsx create mode 100644 web/src/components/atoms/Divider.tsx create mode 100644 web/src/components/atoms/ErrorMessage.tsx create mode 100644 web/src/components/atoms/GoogleButton.tsx create mode 100644 web/src/components/atoms/Input.tsx rename web/src/{ => components}/atoms/Logo.tsx (82%) create mode 100644 web/src/components/atoms/PrivacyNote.tsx create mode 100644 web/src/components/molecules/LoginForm.tsx create mode 100644 web/src/components/molecules/RegisterForm.tsx rename web/src/{ => components}/organisms/LandingCard.tsx (66%) rename web/src/{ => components}/organisms/LoginCard.tsx (57%) rename web/src/{ => components}/organisms/RegisterCard.tsx (57%) create mode 100644 web/src/components/pages/AuthPage.tsx rename web/src/{ => components}/pages/DashboardPage.tsx (76%) rename web/src/{ => components}/templates/AuthLayout.tsx (70%) create mode 100644 web/src/index.css delete mode 100644 web/src/molecules/GoogleButton.tsx delete mode 100644 web/src/molecules/LoginForm.tsx delete mode 100644 web/src/molecules/RegisterForm.tsx delete mode 100644 web/src/pages/AuthPage.tsx create mode 100644 web/src/routes/authRoutes.ts delete mode 100644 web/src/styled.d.ts delete mode 100644 web/src/styles/theme.ts diff --git a/web/package-lock.json b/web/package-lock.json index 34b15ad..07665aa 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,8 +9,6 @@ "version": "0.0.0", "dependencies": { "@fontsource/sen": "^5.2.8", - "@hookform/error-message": "^2.0.1", - "@hookform/resolvers": "^5.2.2", "firebase": "^12.9.0", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -761,23 +759,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -936,15 +917,15 @@ } }, "node_modules/@firebase/ai": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.8.0.tgz", - "integrity": "sha512-grWYGFPsSo+pt+6CYeKR0kWnUfoLLS3xgWPvNrhAS5EPxl6xWq7+HjDZqX24yLneETyl45AVgDsTbVgxeWeRfg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.9.0.tgz", + "integrity": "sha512-NPvBBuvdGo9x3esnABAucFYmqbBmXvyTMimBq2PCuLZbdANZoHzGlx7vfzbwNDaEtCBq4RGGNMliLIv6bZ+PtA==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.3", - "@firebase/component": "0.7.0", + "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -956,15 +937,15 @@ } }, "node_modules/@firebase/analytics": { - "version": "0.10.19", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.19.tgz", - "integrity": "sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg==", + "version": "0.10.20", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.20.tgz", + "integrity": "sha512-adGTNVUWH5q66tI/OQuKLSN6mamPpfYhj0radlH2xt+3eL6NFPtXoOs+ulvs+UsmK27vNFx5FjRDfWk+TyduHg==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -972,15 +953,15 @@ } }, "node_modules/@firebase/analytics-compat": { - "version": "0.2.25", - "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.25.tgz", - "integrity": "sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q==", + "version": "0.2.26", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.26.tgz", + "integrity": "sha512-0j2ruLOoVSwwcXAF53AMoniJKnkwiTjGVfic5LDzqiRkR13vb5j6TXMeix787zbLeQtN/m1883Yv1TxI0gItbA==", "license": "Apache-2.0", "dependencies": { - "@firebase/analytics": "0.10.19", + "@firebase/analytics": "0.10.20", "@firebase/analytics-types": "0.8.3", - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -994,14 +975,14 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app": { - "version": "0.14.8", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.8.tgz", - "integrity": "sha512-WiE9uCGRLUnShdjb9iP20sA3ToWrBbNXr14/N5mow7Nls9dmKgfGaGX5cynLvrltxq2OrDLh1VDNaUgsnS/k/g==", + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.9.tgz", + "integrity": "sha512-3gtUX0e584MYkKBQMgSECMvE1Dwzg+eONefDQ0wxVSe5YMBsZwdN5pL7UapwWBlV8+i8QCztF9TP947tEjZAGA==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", + "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -1010,14 +991,14 @@ } }, "node_modules/@firebase/app-check": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", - "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.1.tgz", + "integrity": "sha512-gmKfwQ2k8aUQlOyRshc+fOQLq0OwUmibIZvpuY1RDNu2ho0aTMlwxOuEiJeYOs7AxzhSx7gnXPFNsXCFbnvXUQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", + "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1028,16 +1009,16 @@ } }, "node_modules/@firebase/app-check-compat": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", - "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.1.tgz", + "integrity": "sha512-yjSvSl5B1u4CirnxhzirN1uiTRCRfx+/qtfbyeyI+8Cx8Cw1RWAIO/OqytPSVwLYbJJ1vEC3EHfxazRaMoWKaA==", "license": "Apache-2.0", "dependencies": { - "@firebase/app-check": "0.11.0", + "@firebase/app-check": "0.11.1", "@firebase/app-check-types": "0.5.3", - "@firebase/component": "0.7.0", + "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1060,15 +1041,15 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app-compat": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.8.tgz", - "integrity": "sha512-4De6SUZ36zozl9kh5rZSxKWULpgty27rMzZ6x+xkoo7+NWyhWyFdsdvhFsWhTw/9GGj0wXIcbTjwHYCUIUuHyg==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.9.tgz", + "integrity": "sha512-e5LzqjO69/N2z7XcJeuMzIp4wWnW696dQeaHAUpQvGk89gIWHAIvG6W+mA3UotGW6jBoqdppEJ9DnuwbcBByug==", "license": "Apache-2.0", "dependencies": { - "@firebase/app": "0.14.8", - "@firebase/component": "0.7.0", + "@firebase/app": "0.14.9", + "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1082,14 +1063,14 @@ "license": "Apache-2.0" }, "node_modules/@firebase/auth": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.0.tgz", - "integrity": "sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.1.tgz", + "integrity": "sha512-nXKj7d5bMBlnq6XpcQQpmnSVwEeHBkoVbY/+Wk0P1ebLSICoH4XPtvKOFlXKfIHmcS84mLQ99fk3njlDGKSDtw==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", + "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1106,15 +1087,15 @@ } }, "node_modules/@firebase/auth-compat": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.2.tgz", - "integrity": "sha512-8UhCzF6pav9bw/eXA8Zy1QAKssPRYEYXaWagie1ewLTwHkXv6bKp/j6/IwzSYQP67sy/BMFXIFaCCsoXzFLr7A==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.3.tgz", + "integrity": "sha512-nHOkupcYuGVxI1AJJ/OBhLPaRokbP14Gq4nkkoVvf1yvuREEWqdnrYB/CdsSnPxHMAnn5wJIKngxBF9jNX7s/Q==", "license": "Apache-2.0", "dependencies": { - "@firebase/auth": "1.12.0", + "@firebase/auth": "1.12.1", "@firebase/auth-types": "0.13.0", - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1141,12 +1122,12 @@ } }, "node_modules/@firebase/component": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", - "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.1.tgz", + "integrity": "sha512-mFzsm7CLHR60o08S23iLUY8m/i6kLpOK87wdEFPLhdlCahaxKmWOwSVGiWoENYSmFJJoDhrR3gKSCxz7ENdIww==", "license": "Apache-2.0", "dependencies": { - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1154,15 +1135,15 @@ } }, "node_modules/@firebase/data-connect": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.12.tgz", - "integrity": "sha512-baPddcoNLj/+vYo+HSJidJUdr5W4OkhT109c5qhR8T1dJoZcyJpkv/dFpYlw/VJ3dV66vI8GHQFrmAZw/xUS4g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.4.0.tgz", + "integrity": "sha512-vLXM6WHNIR3VtEeYNUb/5GTsUOyl3Of4iWNZHBe1i9f88sYFnxybJNWVBjvJ7flhCyF8UdxGpzWcUnv6F5vGfg==", "license": "Apache-2.0", "dependencies": { "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.0", + "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1170,16 +1151,16 @@ } }, "node_modules/@firebase/database": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", - "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.1.tgz", + "integrity": "sha512-LwIXe8+mVHY5LBPulWECOOIEXDiatyECp/BOlu0gOhe+WOcKjWHROaCbLlkFTgHMY7RHr5MOxkLP/tltWAH3dA==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.0", + "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" }, @@ -1188,16 +1169,16 @@ } }, "node_modules/@firebase/database-compat": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", - "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.1.tgz", + "integrity": "sha512-heAEVZ9Z8c8PnBUcmGh91JHX0cXcVa1yESW/xkLuwaX7idRFyLiN8sl73KXpR8ZArGoPXVQDanBnk6SQiekRCQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/database": "1.1.0", - "@firebase/database-types": "1.0.16", + "@firebase/component": "0.7.1", + "@firebase/database": "1.1.1", + "@firebase/database-types": "1.0.17", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1205,24 +1186,24 @@ } }, "node_modules/@firebase/database-types": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", - "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.17.tgz", + "integrity": "sha512-4eWaM5fW3qEIHjGzfi3cf0Jpqi1xQsAdT6rSDE1RZPrWu8oGjgrq6ybMjobtyHQFgwGCykBm4YM89qDzc+uG/w==", "license": "Apache-2.0", "dependencies": { "@firebase/app-types": "0.9.3", - "@firebase/util": "1.13.0" + "@firebase/util": "1.14.0" } }, "node_modules/@firebase/firestore": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.11.0.tgz", - "integrity": "sha512-Zb88s8rssBd0J2Tt+NUXMPt2sf+Dq7meatKiJf5t9oto1kZ8w9gK59Koe1uPVbaKfdgBp++N/z0I4G/HamyEhg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.12.0.tgz", + "integrity": "sha512-PM47OyiiAAoAMB8kkq4Je14mTciaRoAPDd3ng3Ckqz9i2TX9D9LfxIRcNzP/OxzNV4uBKRq6lXoOggkJBQR3Gw==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", + "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "@firebase/webchannel-wrapper": "1.0.5", "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.8", @@ -1236,15 +1217,15 @@ } }, "node_modules/@firebase/firestore-compat": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.5.tgz", - "integrity": "sha512-yVX1CkVvqBI4qbA56uZo42xFA4TNU0ICQ+9AFDvYq9U9Xu6iAx9lFDAk/tN+NGereQQXXCSnpISwc/oxsQqPLA==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.6.tgz", + "integrity": "sha512-NgVyR4hHHN2FvSNQOtbgBOuVsEdD/in30d9FKbEvvITiAChrBN2nBstmhfjI4EOTnHaP8zigwvkNYFI9yKGAkQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/firestore": "4.11.0", + "@firebase/component": "0.7.1", + "@firebase/firestore": "4.12.0", "@firebase/firestore-types": "3.0.3", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1265,16 +1246,16 @@ } }, "node_modules/@firebase/functions": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.1.tgz", - "integrity": "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.2.tgz", + "integrity": "sha512-tHduUD+DeokM3NB1QbHCvEMoL16e8Z8JSkmuVA4ROoJKPxHn8ibnecHPO2e3nVCJR1D9OjuKvxz4gksfq92/ZQ==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.0", + "@firebase/component": "0.7.1", "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1285,15 +1266,15 @@ } }, "node_modules/@firebase/functions-compat": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.1.tgz", - "integrity": "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.2.tgz", + "integrity": "sha512-YNxgnezvZDkqxqXa6cT7/oTeD4WXbxgIP7qZp4LFnathQv5o2omM6EoIhXiT9Ie5AoQDcIhG9Y3/dj+DFJGaGQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/functions": "0.13.1", + "@firebase/component": "0.7.1", + "@firebase/functions": "0.13.2", "@firebase/functions-types": "0.6.3", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1310,13 +1291,13 @@ "license": "Apache-2.0" }, "node_modules/@firebase/installations": { - "version": "0.6.19", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", - "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", + "version": "0.6.20", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.20.tgz", + "integrity": "sha512-LOzvR7XHPbhS0YB5ANXhqXB5qZlntPpwU/4KFwhSNpXNsGk/sBQ9g5hepi0y0/MfenJLe2v7t644iGOOElQaHQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -1325,15 +1306,15 @@ } }, "node_modules/@firebase/installations-compat": { - "version": "0.2.19", - "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", - "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.20.tgz", + "integrity": "sha512-9C9pL/DIEGucmoPj8PlZTnztbX3nhNj5RTYVpUM7wQq/UlHywaYv99969JU/WHLvi9ptzIogXYS9d1eZ6XFe9g==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", "@firebase/installations-types": "0.5.3", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1362,15 +1343,15 @@ } }, "node_modules/@firebase/messaging": { - "version": "0.12.23", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", - "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", + "version": "0.12.24", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.24.tgz", + "integrity": "sha512-UtKoubegAhHyehcB7iQjvQ8OVITThPbbWk3g2/2ze42PrQr6oe6OmCElYQkBrE5RDCeMTNucXejbdulrQ2XwVg==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -1379,14 +1360,14 @@ } }, "node_modules/@firebase/messaging-compat": { - "version": "0.2.23", - "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", - "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.24.tgz", + "integrity": "sha512-wXH8FrKbJvFuFe6v98TBhAtvgknxKIZtGM/wCVsfpOGmaAE80bD8tBxztl+uochjnFb9plihkd6mC4y7sZXSpA==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/messaging": "0.12.23", - "@firebase/util": "1.13.0", + "@firebase/component": "0.7.1", + "@firebase/messaging": "0.12.24", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1400,15 +1381,15 @@ "license": "Apache-2.0" }, "node_modules/@firebase/performance": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz", - "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==", + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.10.tgz", + "integrity": "sha512-8nRFld+Ntzp5cLKzZuG9g+kBaSn8Ks9dmn87UQGNFDygbmR6ebd8WawauEXiJjMj1n70ypkvAOdE+lzeyfXtGA==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0", "web-vitals": "^4.2.4" }, @@ -1417,16 +1398,16 @@ } }, "node_modules/@firebase/performance-compat": { - "version": "0.2.22", - "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz", - "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==", + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.23.tgz", + "integrity": "sha512-c7qOAGBUAOpIuUlHu1axWcrCVtIYKPMhH0lMnoCDWnPwn1HcPuPUBVTWETbC7UWw71RMJF8DpirfWXzMWJQfgA==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", + "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", - "@firebase/performance": "0.7.9", + "@firebase/performance": "0.7.10", "@firebase/performance-types": "0.2.3", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1440,15 +1421,15 @@ "license": "Apache-2.0" }, "node_modules/@firebase/remote-config": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.0.tgz", - "integrity": "sha512-sJz7C2VACeE257Z/3kY9Ap2WXbFsgsDLfaGfZmmToKAK39ipXxFan+vzB9CSbF6mP7bzjyzEnqPcMXhAnYE6fQ==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.1.tgz", + "integrity": "sha512-L86TReBnPiiJOWd7k9iaiE9f7rHtMpjAoYN0fH2ey2ZRzsOChHV0s5sYf1+IIUYzplzsE46pjlmAUNkRRKwHSQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1456,16 +1437,16 @@ } }, "node_modules/@firebase/remote-config-compat": { - "version": "0.2.21", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.21.tgz", - "integrity": "sha512-9+lm0eUycxbu8GO25JfJe4s6R2xlDqlVt0CR6CvN9E6B4AFArEV4qfLoDVRgIEB7nHKwvH2nYRocPWfmjRQTnw==", + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.22.tgz", + "integrity": "sha512-uW/eNKKtRBot2gnCC5mnoy5Voo2wMzZuQ7dwqqGHU176fO9zFgMwKiRzk+aaC99NLrFk1KOmr0ZVheD+zdJmjQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", + "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", - "@firebase/remote-config": "0.8.0", + "@firebase/remote-config": "0.8.1", "@firebase/remote-config-types": "0.5.0", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1479,13 +1460,13 @@ "license": "Apache-2.0" }, "node_modules/@firebase/storage": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", - "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.1.tgz", + "integrity": "sha512-uIpYgBBsv1vIET+5xV20XT7wwqV+H4GFp6PBzfmLUcEgguS4SWNFof56Z3uOC2lNDh0KDda1UflYq2VwD9Nefw==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1496,15 +1477,15 @@ } }, "node_modules/@firebase/storage-compat": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", - "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.1.tgz", + "integrity": "sha512-bgl3FHHfXAmBgzIK/Fps6Xyv2HiAQlSTov07CBL+RGGhrC5YIk4lruS8JVIC+UkujRdYvnf8cpQFGn2RCilJ/A==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/storage": "0.14.0", + "@firebase/component": "0.7.1", + "@firebase/storage": "0.14.1", "@firebase/storage-types": "0.8.3", - "@firebase/util": "1.13.0", + "@firebase/util": "1.14.0", "tslib": "^2.1.0" }, "engines": { @@ -1525,9 +1506,9 @@ } }, "node_modules/@firebase/util": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", - "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.14.0.tgz", + "integrity": "sha512-/gnejm7MKkVIXnSJGpc9L2CvvvzJvtDPeAEq5jAwgVlf/PeNxot+THx/bpD20wQ8uL5sz0xqgXy1nisOYMU+mw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -1583,29 +1564,6 @@ "node": ">=6" } }, - "node_modules/@hookform/error-message": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.1.tgz", - "integrity": "sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "react-hook-form": "^7.0.0" - } - }, - "node_modules/@hookform/resolvers": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", - "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", - "license": "MIT", - "dependencies": { - "@standard-schema/utils": "^0.3.0" - }, - "peerDependencies": { - "react-hook-form": "^7.55.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2115,26 +2073,6 @@ "win32" ] }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2195,9 +2133,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", - "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "version": "24.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", + "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -2428,9 +2366,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { @@ -2441,9 +2379,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", - "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2701,9 +2639,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001774", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", - "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "version": "1.0.30001775", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", + "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", "dev": true, "funding": [ { @@ -2918,6 +2856,23 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3206,39 +3161,39 @@ } }, "node_modules/firebase": { - "version": "12.9.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.9.0.tgz", - "integrity": "sha512-CwwTYoqZg6KxygPOaaJqIc4aoLvo0RCRrXoln9GoxLE8QyAwTydBaSLGVlR4WPcuOgN3OEL0tJLT1H4IU/dv7w==", + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.10.0.tgz", + "integrity": "sha512-tAjHnEirksqWpa+NKDUSUMjulOnsTcsPC1X1rQ+gwPtjlhJS572na91CwaBXQJHXharIrfj7sw/okDkXOsphjA==", "license": "Apache-2.0", "dependencies": { - "@firebase/ai": "2.8.0", - "@firebase/analytics": "0.10.19", - "@firebase/analytics-compat": "0.2.25", - "@firebase/app": "0.14.8", - "@firebase/app-check": "0.11.0", - "@firebase/app-check-compat": "0.4.0", - "@firebase/app-compat": "0.5.8", + "@firebase/ai": "2.9.0", + "@firebase/analytics": "0.10.20", + "@firebase/analytics-compat": "0.2.26", + "@firebase/app": "0.14.9", + "@firebase/app-check": "0.11.1", + "@firebase/app-check-compat": "0.4.1", + "@firebase/app-compat": "0.5.9", "@firebase/app-types": "0.9.3", - "@firebase/auth": "1.12.0", - "@firebase/auth-compat": "0.6.2", - "@firebase/data-connect": "0.3.12", - "@firebase/database": "1.1.0", - "@firebase/database-compat": "2.1.0", - "@firebase/firestore": "4.11.0", - "@firebase/firestore-compat": "0.4.5", - "@firebase/functions": "0.13.1", - "@firebase/functions-compat": "0.4.1", - "@firebase/installations": "0.6.19", - "@firebase/installations-compat": "0.2.19", - "@firebase/messaging": "0.12.23", - "@firebase/messaging-compat": "0.2.23", - "@firebase/performance": "0.7.9", - "@firebase/performance-compat": "0.2.22", - "@firebase/remote-config": "0.8.0", - "@firebase/remote-config-compat": "0.2.21", - "@firebase/storage": "0.14.0", - "@firebase/storage-compat": "0.4.0", - "@firebase/util": "1.13.0" + "@firebase/auth": "1.12.1", + "@firebase/auth-compat": "0.6.3", + "@firebase/data-connect": "0.4.0", + "@firebase/database": "1.1.1", + "@firebase/database-compat": "2.1.1", + "@firebase/firestore": "4.12.0", + "@firebase/firestore-compat": "0.4.6", + "@firebase/functions": "0.13.2", + "@firebase/functions-compat": "0.4.2", + "@firebase/installations": "0.6.20", + "@firebase/installations-compat": "0.2.20", + "@firebase/messaging": "0.12.24", + "@firebase/messaging-compat": "0.2.24", + "@firebase/performance": "0.7.10", + "@firebase/performance-compat": "0.2.23", + "@firebase/remote-config": "0.8.1", + "@firebase/remote-config-compat": "0.2.22", + "@firebase/storage": "0.14.1", + "@firebase/storage-compat": "0.4.1", + "@firebase/util": "1.14.0" } }, "node_modules/flat-cache": { @@ -3256,9 +3211,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", "dev": true, "license": "ISC" }, @@ -3574,9 +3529,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3728,10 +3683,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -3748,7 +3702,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3827,23 +3781,6 @@ "react": "^19.2.4" } }, - "node_modules/react-hook-form": { - "version": "7.71.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", - "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18 || ^19" - } - }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3956,6 +3893,20 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4108,34 +4059,6 @@ } } }, - "node_modules/styled-components/node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/stylis": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", @@ -4364,6 +4287,35 @@ } } }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/web-vitals": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", diff --git a/web/package.json b/web/package.json index 4b19ee7..9146b71 100644 --- a/web/package.json +++ b/web/package.json @@ -11,8 +11,6 @@ }, "dependencies": { "@fontsource/sen": "^5.2.8", - "@hookform/error-message": "^2.0.1", - "@hookform/resolvers": "^5.2.2", "firebase": "^12.9.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/web/src/App.tsx b/web/src/App.tsx index 48ef811..ed03e72 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,46 +1,64 @@ -import React from 'react' -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' -import { ThemeProvider } from 'styled-components' -import { AuthProvider } from './context/AuthContext' -import { ProtectedRoute, PublicRoute } from './guards/RouteGuards' -import { AuthPage } from './pages/AuthPage' -import { DashboardPage } from './pages/DashboardPage' -import { GlobalStyles, theme } from './styles/theme' +import React from 'react'; +import { + createBrowserRouter, + RouterProvider, + Navigate, +} from 'react-router-dom'; +import { AuthProvider } from './context/AuthContext'; +import { ProtectedRoute, PublicRoute } from './guards/RouteGuards'; +import { AuthPage } from './components/pages/AuthPage'; +import { DashboardPage } from './components/pages/DashboardPage'; +import { LandingCard } from './components/organisms/LandingCard'; +import { LoginCard } from './components/organisms/LoginCard'; +import { RegisterCard } from './components/organisms/RegisterCard'; +import { AUTH_ROUTES, AUTH_ROUTE_SEGMENTS } from './routes/authRoutes'; -const App: React.FC = () => { - return ( - - - - - - {/* Public: redirect to dashboard if already logged in */} - - - - } - /> - - {/* Protected: redirect to /auth if not logged in */} - - - - } - /> +const router = createBrowserRouter([ + { + path: AUTH_ROUTES.base, + element: , + children: [ + { + element: , + children: [ + { + index: true, + element: , + }, + { + path: AUTH_ROUTE_SEGMENTS.login, + element: , + }, + { + path: AUTH_ROUTE_SEGMENTS.register, + element: , + }, + ], + }, + ], + }, + { + path: '/dashboard', + element: , + children: [ + { + index: true, + element: , + }, + ], + }, + { + path: '*', + element: , + }, +]); - {/* Default redirect */} - } /> - - - - - ) -} +const App: React.FC = () => { + return ( + + + + ); +}; -export default App +export default App; diff --git a/web/src/atoms/Button.tsx b/web/src/atoms/Button.tsx deleted file mode 100644 index 264bcae..0000000 --- a/web/src/atoms/Button.tsx +++ /dev/null @@ -1,136 +0,0 @@ -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` - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - width: ${({ $fullWidth }) => ($fullWidth ? '100%' : '173px')}; - padding: 12px 24px; - border-radius: ${({ theme }) => theme.radii.md}; - font-size: 0.875rem; - font-weight: 500; - letter-spacing: 0.02em; - transition: all ${({ theme }) => theme.transitions.normal}; - position: relative; - overflow: hidden; - - &:disabled { - opacity: 0.55; - cursor: not-allowed; - } - - &::after { - content: ''; - position: absolute; - inset: 0; - background: white; - opacity: 0; - transition: opacity ${({ theme }) => theme.transitions.fast}; - } - - &:not(:disabled):hover::after { - opacity: 0.06; - } - - &:not(:disabled):active::after { - opacity: 0.1; - } - - ${({ $variant, theme }) => - $variant === 'primary' && - css` - background: ${theme.colors.red}; - color: ${theme.colors.white}; - box-shadow: 0 4px 20px hsla(6, 63%, 46%, 0.35); - - &:not(:disabled):hover { - background: ${theme.colors.redHover}; - box-shadow: 0 8px 20px rgba(94, 25, 25, 0.35); - - transform: translateY(-1px); - } - `} - - ${({ $variant, theme }) => - $variant === 'secondary' && - css` - background: ${theme.colors.bgInput}; - color: ${theme.colors.text}; - border: 1px solid ${theme.colors.border}; - - &:not(:disabled):hover { - border-color: ${theme.colors.muted}; - transform: translateY(-1px); - } - `} - - ${({ $variant, theme }) => - $variant === 'google' && - css` - background: ${theme.colors.google}; - color: ${theme.colors.text}; - border: 1px solid ${theme.colors.border}; - - &:not(:disabled):hover { - border-color: ${theme.colors.muted}; - transform: translateY(-1px); - } - `} -` - -const Spinner = styled.span` - width: 16px; - height: 16px; - border: 2px solid rgba(255, 255, 255, 0.3); - border-top-color: white; - 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 = ({ - variant = 'primary', - fullWidth = false, - loading = false, - disabled, - onClick, - type = 'button', - children, -}) => { - return ( - - {loading ? : children} - - ) -} diff --git a/web/src/atoms/Divider.tsx b/web/src/atoms/Divider.tsx deleted file mode 100644 index dcb15d6..0000000 --- a/web/src/atoms/Divider.tsx +++ /dev/null @@ -1,34 +0,0 @@ -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: ${({ theme }) => theme.colors.overmuted}; -` - -const Label = styled.span` - font-size: 15px; - color: ${({ theme }) => theme.colors.white}; - font-weight: 400; -` - -interface DividerProps { - label?: string -} - -export const Divider: React.FC = ({ label = 'Or' }) => ( - - - - - -) diff --git a/web/src/atoms/ErrorMessage.tsx b/web/src/atoms/ErrorMessage.tsx deleted file mode 100644 index a050d07..0000000 --- a/web/src/atoms/ErrorMessage.tsx +++ /dev/null @@ -1,18 +0,0 @@ -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: #e57373; - animation: ${slideIn} 0.2s ease; - line-height: 0.5; -` - -export const ErrorMessage: React.FC<{ message: string }> = ({ message }) => ( - {message} -) diff --git a/web/src/atoms/Input.tsx b/web/src/atoms/Input.tsx deleted file mode 100644 index 321a0b0..0000000 --- a/web/src/atoms/Input.tsx +++ /dev/null @@ -1,50 +0,0 @@ -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; - background: ${({ theme }) => theme.colors.bgInput}; - border: 1px solid ${({ theme }) => theme.colors.border}; - border-radius: ${({ theme }) => theme.radii.md}; - padding: 14px 16px; - font-size: 0.9rem; - color: ${({ theme }) => theme.colors.text}; - transition: border-color ${({ theme }) => theme.transitions.normal}, - box-shadow ${({ theme }) => theme.transitions.normal}; - - &::placeholder { - color: ${({ theme }) => theme.colors.muted}; - } - - - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } -` - -interface InputProps extends React.InputHTMLAttributes { - error?: boolean -} - -export const Input = React.forwardRef( - ({ error: _error, ...rest }, ref) => { - return ( - - - - ) - } -) - -Input.displayName = 'Input' diff --git a/web/src/atoms/LoadingScreen.tsx b/web/src/atoms/LoadingScreen.tsx deleted file mode 100644 index 515da5d..0000000 --- a/web/src/atoms/LoadingScreen.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react' -import styled, { keyframes } from 'styled-components' - -const spin = keyframes` - to { transform: rotate(360deg); } -` - -const Wrapper = styled.div` - height: 100vh; - width: 100vw; - background: ${({ theme }) => theme.colors.bg}; - display: flex; - align-items: center; - justify-content: center; -` - -const Ring = styled.div` - width: 40px; - height: 40px; - border: 3px solid ${({ theme }) => theme.colors.border}; - border-top-color: ${({ theme }) => theme.colors.red}; - border-radius: 50%; - animation: ${spin} 0.8s linear infinite; -` - -export const LoadingScreen: React.FC = () => ( - - - -) diff --git a/web/src/atoms/PrivacyNote.tsx b/web/src/atoms/PrivacyNote.tsx deleted file mode 100644 index 3ec23a3..0000000 --- a/web/src/atoms/PrivacyNote.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react' -import styled from 'styled-components' - -const Text = styled.p` - font-size: 15px; - color: ${({ theme }) => theme.colors.overmuted}; - text-align: center; - line-height: 1.5; - - a { - color: ${({ theme }) => theme.colors.overmuted}; - text-decoration: underline; - text-underline-offset: 2px; - - &:hover { - color: ${({ theme }) => theme.colors.white}; - } - } -` - -export const PrivacyNote: React.FC = () => ( - - By continuing, you agree to our{' '} - - Privacy Policy - - -) diff --git a/web/src/components/atoms/Button.tsx b/web/src/components/atoms/Button.tsx new file mode 100644 index 0000000..6b33027 --- /dev/null +++ b/web/src/components/atoms/Button.tsx @@ -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` + 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(-1px); + } + 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(-1px); + } + `} + + ${({ $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(-1px); + } + `} +`; + +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 = ({ + variant = 'primary', + fullWidth = false, + loading = false, + disabled, + onClick, + type = 'button', + children, +}) => { + return ( + + {loading ? : children} + + ); +}; diff --git a/web/src/components/atoms/Divider.tsx b/web/src/components/atoms/Divider.tsx new file mode 100644 index 0000000..d62187d --- /dev/null +++ b/web/src/components/atoms/Divider.tsx @@ -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 = ({ label = 'Or' }) => ( + + + + + +); diff --git a/web/src/components/atoms/ErrorMessage.tsx b/web/src/components/atoms/ErrorMessage.tsx new file mode 100644 index 0000000..d06dfb1 --- /dev/null +++ b/web/src/components/atoms/ErrorMessage.tsx @@ -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.5; + text-align: center; +`; + +export const ErrorMessage: React.FC<{ message: string }> = ({ message }) => ( + {message} +); diff --git a/web/src/components/atoms/GoogleButton.tsx b/web/src/components/atoms/GoogleButton.tsx new file mode 100644 index 0000000..7c28e5a --- /dev/null +++ b/web/src/components/atoms/GoogleButton.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button } from './Button'; + +const GoogleIcon: React.FC = () => ( + + + + + + +); + +interface GoogleButtonProps { + onClick: () => void; + loading?: boolean; +} + +export const GoogleButton: React.FC = ({ + onClick, + loading, +}) => ( + +); diff --git a/web/src/components/atoms/Input.tsx b/web/src/components/atoms/Input.tsx new file mode 100644 index 0000000..4429dc2 --- /dev/null +++ b/web/src/components/atoms/Input.tsx @@ -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 { + error?: boolean; +} + +export const Input = React.forwardRef( + ({ error: _error, ...rest }, ref) => { + return ( + + + + ); + }, +); + +Input.displayName = 'Input'; diff --git a/web/src/atoms/Logo.tsx b/web/src/components/atoms/Logo.tsx similarity index 82% rename from web/src/atoms/Logo.tsx rename to web/src/components/atoms/Logo.tsx index bfc8708..d6dc9b0 100644 --- a/web/src/atoms/Logo.tsx +++ b/web/src/components/atoms/Logo.tsx @@ -20,17 +20,13 @@ const LogoImage = styled.img` const Title = styled.h1` - font-family: ${({ theme }) => theme.fonts.display}; font-size: 2.4rem; letter-spacing: 0.04em; - color: ${({ theme }) => theme.colors.white}; line-height: 1; `; const Subtitle = styled.p` font-size:20px; - color: ${({ theme }) => theme.colors.textSub}; - font-weight: 300; letter-spacing: 0.02em; `; diff --git a/web/src/components/atoms/PrivacyNote.tsx b/web/src/components/atoms/PrivacyNote.tsx new file mode 100644 index 0000000..dba4bb8 --- /dev/null +++ b/web/src/components/atoms/PrivacyNote.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +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 = () => ( + + By continuing, you agree to our{' '} + + Privacy Policy + + +); diff --git a/web/src/components/molecules/LoginForm.tsx b/web/src/components/molecules/LoginForm.tsx new file mode 100644 index 0000000..33a53a4 --- /dev/null +++ b/web/src/components/molecules/LoginForm.tsx @@ -0,0 +1,148 @@ +import React, { useState } from 'react' +import styled from 'styled-components' +import { Input } from '../atoms/Input' +import { Button } from '../atoms/Button' +import { ErrorMessage } from '../atoms/ErrorMessage' +import { useAuth } from '../../context/AuthContext' + +const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + width: 100%; + & > :last-child { + margin-top: 20px; + } +` + +interface LoginFormProps { + onSuccess?: () => void +} + +interface LoginFields { + email: string + password: string +} + +export const LoginForm: React.FC = ({ onSuccess }) => { + const { loginWithEmail, error, clearError } = useAuth() + + const [form, setForm] = useState({ + email: '', + password: '', + }) + + const [errors, setErrors] = useState>({}) + const [isSubmitting, setIsSubmitting] = useState(false) + const [hasFailedSubmit, setHasFailedSubmit] = useState(false) + + // Handle input change + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + + setForm(prev => ({ + ...prev, + [name]: value, + })) + + // If user previously failed submit → revalidate live + if (hasFailedSubmit) { + validateField(name as keyof LoginFields, value) + } + } + + const validateField = (field: keyof LoginFields, value: string) => { + let message = '' + + if (field === 'email') { + if (!value) message = 'Email is required' + else if (!/\S+@\S+\.\S+/.test(value)) message = 'Enter a valid email' + } + + if (field === 'password') { + if (!value) message = 'Password is required' + } + + setErrors(prev => ({ + ...prev, + [field]: message, + })) + } + + const validateForm = () => { + const newErrors: Partial = {} + + if (!form.email) newErrors.email = 'Email is required' + else if (!/\S+@\S+\.\S+/.test(form.email)) + newErrors.email = 'Enter a valid email' + + if (!form.password) newErrors.password = 'Password is required' + + setErrors(newErrors) + + return Object.keys(newErrors).length === 0 + } + + const onSubmit = async (e: React.SubmitEvent) => { + e.preventDefault() + clearError() + + const isValid = validateForm() + + if (!isValid) { + setHasFailedSubmit(true) + return + } + + try { + setIsSubmitting(true) + await loginWithEmail(form.email, form.password) + onSuccess?.() + } catch (err) { + console.error('Login failed', err) + } finally { + setIsSubmitting(false) + } + } + + const isLockedOut = + hasFailedSubmit && + (!!errors.email || !!errors.password) + + return ( +
+ {error && } + + + {errors.email && } + + + {errors.password && } + + + + ) +} \ No newline at end of file diff --git a/web/src/components/molecules/RegisterForm.tsx b/web/src/components/molecules/RegisterForm.tsx new file mode 100644 index 0000000..9014a41 --- /dev/null +++ b/web/src/components/molecules/RegisterForm.tsx @@ -0,0 +1,180 @@ +import React, { useState } from 'react' +import styled from 'styled-components' +import { Input } from '../atoms/Input' +import { Button } from '../atoms/Button' +import { ErrorMessage } from '../atoms/ErrorMessage' +import { useAuth } from '../../context/AuthContext' + +const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + width: 100%; + & > :last-child { + margin-top: 16px; + } +` + +interface RegisterFields { + email: string + password: string + repeatPassword: string +} + +export const RegisterForm: React.FC = () => { + const { registerWithEmail, error, clearError } = useAuth() + + const [form, setForm] = useState({ + email: '', + password: '', + repeatPassword: '', + }) + + const [errors, setErrors] = useState>({}) + const [isSubmitting, setIsSubmitting] = useState(false) + const [hasFailedSubmit, setHasFailedSubmit] = useState(false) + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + + setForm(prev => ({ + ...prev, + [name]: value, + })) + + if (hasFailedSubmit) { + validateField(name as keyof RegisterFields, value) + } + } + + const validateField = ( + field: keyof RegisterFields, + value: string + ) => { + let message = '' + + if (field === 'email') { + if (!value) message = 'Email is required' + else if (!/\S+@\S+\.\S+/.test(value)) + message = 'Enter a valid email' + } + + if (field === 'password') { + if (!value) message = 'Password is required' + else if (value.length < 8) + message = 'Password must be at least 8 characters' + } + + if (field === 'repeatPassword') { + if (!value) message = 'Please confirm your password' + else if (value !== form.password) + message = 'Passwords do not match' + } + + setErrors(prev => ({ + ...prev, + [field]: message, + })) + } + + const validateForm = () => { + const newErrors: Partial = {} + + if (!form.email) newErrors.email = 'Email is required' + else if (!/\S+@\S+\.\S+/.test(form.email)) + newErrors.email = 'Enter a valid email' + + if (!form.password) + newErrors.password = 'Password is required' + else if (form.password.length < 8) + newErrors.password = + 'Password must be at least 8 characters' + + if (!form.repeatPassword) + newErrors.repeatPassword = + 'Please confirm your password' + else if (form.password !== form.repeatPassword) + newErrors.repeatPassword = + 'Passwords do not match' + + setErrors(newErrors) + + return Object.keys(newErrors).length === 0 + } + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault() + clearError() + + const isValid = validateForm() + + if (!isValid) { + setHasFailedSubmit(true) + return + } + + try { + setIsSubmitting(true) + await registerWithEmail(form.email, form.password) + } catch (err) { + console.error('Registration failed', err) + } finally { + setIsSubmitting(false) + } + } + + const isLockedOut = + hasFailedSubmit && + (!!errors.email || + !!errors.password || + !!errors.repeatPassword) + + return ( +
+ {error && } + + + {errors.email && } + + + {errors.password && } + + + {errors.repeatPassword && ( + + )} + + + + ) +} \ No newline at end of file diff --git a/web/src/organisms/LandingCard.tsx b/web/src/components/organisms/LandingCard.tsx similarity index 66% rename from web/src/organisms/LandingCard.tsx rename to web/src/components/organisms/LandingCard.tsx index d44150f..c04474f 100644 --- a/web/src/organisms/LandingCard.tsx +++ b/web/src/components/organisms/LandingCard.tsx @@ -2,10 +2,11 @@ import React, { useState } from 'react' import styled from 'styled-components' import { Button } from '../atoms/Button' import { Divider } from '../atoms/Divider' -import { GoogleButton } from '../molecules/GoogleButton' -import { useAuth } from '../context/AuthContext' +import { GoogleButton } from '../atoms/GoogleButton' +import { useAuth } from '../../context/AuthContext' import { ErrorMessage } from '../atoms/ErrorMessage' -import type { AuthView } from '../types' +import { useNavigate } from 'react-router-dom' +import { AUTH_ROUTES } from '../../routes/authRoutes' const Actions = styled.div` display: flex; @@ -14,12 +15,9 @@ const Actions = styled.div` ` -interface LandingCardProps { - onViewChange: (view: AuthView) => void -} - -export const LandingCard: React.FC = ({ onViewChange }) => { +export const LandingCard: React.FC = () => { const { loginWithGoogle, error, clearError } = useAuth() + const navigate = useNavigate() const [googleLoading, setGoogleLoading] = useState(false) const handleGoogle = async () => { @@ -37,10 +35,10 @@ export const LandingCard: React.FC = ({ onViewChange }) => { return ( {error && } - - diff --git a/web/src/organisms/LoginCard.tsx b/web/src/components/organisms/LoginCard.tsx similarity index 57% rename from web/src/organisms/LoginCard.tsx rename to web/src/components/organisms/LoginCard.tsx index 8fb13eb..a98f126 100644 --- a/web/src/organisms/LoginCard.tsx +++ b/web/src/components/organisms/LoginCard.tsx @@ -1,9 +1,6 @@ import React from 'react' import styled from 'styled-components' import { LoginForm } from '../molecules/LoginForm' -import type { AuthView } from '../types' - - const Wrapper = styled.div` display: flex; @@ -11,11 +8,7 @@ const Wrapper = styled.div` width: 100%; ` -interface LoginCardProps { - onViewChange: (view: AuthView) => void -} - -export const LoginCard: React.FC = ({ onViewChange }) => ( +export const LoginCard: React.FC = () => ( diff --git a/web/src/organisms/RegisterCard.tsx b/web/src/components/organisms/RegisterCard.tsx similarity index 57% rename from web/src/organisms/RegisterCard.tsx rename to web/src/components/organisms/RegisterCard.tsx index 76a3834..ed7b643 100644 --- a/web/src/organisms/RegisterCard.tsx +++ b/web/src/components/organisms/RegisterCard.tsx @@ -1,9 +1,6 @@ import React from 'react' import styled from 'styled-components' import { RegisterForm } from '../molecules/RegisterForm' -import type { AuthView } from '../types' - - const Wrapper = styled.div` display: flex; @@ -12,11 +9,7 @@ const Wrapper = styled.div` width: 100%; ` -interface RegisterCardProps { - onViewChange: (view: AuthView) => void -} - -export const RegisterCard: React.FC = ({ onViewChange }) => ( +export const RegisterCard: React.FC = () => ( diff --git a/web/src/components/pages/AuthPage.tsx b/web/src/components/pages/AuthPage.tsx new file mode 100644 index 0000000..730fa7c --- /dev/null +++ b/web/src/components/pages/AuthPage.tsx @@ -0,0 +1,4 @@ +import React from 'react' +import { AuthLayout } from '../templates/AuthLayout' + +export const AuthPage: React.FC = () => diff --git a/web/src/pages/DashboardPage.tsx b/web/src/components/pages/DashboardPage.tsx similarity index 76% rename from web/src/pages/DashboardPage.tsx rename to web/src/components/pages/DashboardPage.tsx index 7b1e8b7..b5bfc9e 100644 --- a/web/src/pages/DashboardPage.tsx +++ b/web/src/components/pages/DashboardPage.tsx @@ -1,11 +1,10 @@ import React from 'react' import styled from 'styled-components' -import { useAuth } from '../context/AuthContext' +import { useAuth } from '../../context/AuthContext' import { Button } from '../atoms/Button' const Page = styled.div` min-height: 100vh; - background: ${({ theme }) => theme.colors.bg}; display: flex; flex-direction: column; align-items: center; @@ -15,14 +14,14 @@ const Page = styled.div` ` const Title = styled.h1` - font-family: ${({ theme }) => theme.fonts.display}; + font-size: 3rem; - color: ${({ theme }) => theme.colors.white}; + letter-spacing: 0.04em; ` const Subtitle = styled.p` - color: ${({ theme }) => theme.colors.textSub}; + font-size: 1rem; ` @@ -30,7 +29,7 @@ const Badge = styled.span` background: rgba(192, 57, 43, 0.15); border: 1px solid rgba(192, 57, 43, 0.3); color: #e57373; - border-radius: ${({ theme }) => theme.radii.full}; + padding: 4px 14px; font-size: 0.8rem; font-weight: 500; diff --git a/web/src/templates/AuthLayout.tsx b/web/src/components/templates/AuthLayout.tsx similarity index 70% rename from web/src/templates/AuthLayout.tsx rename to web/src/components/templates/AuthLayout.tsx index c949205..94e0093 100644 --- a/web/src/templates/AuthLayout.tsx +++ b/web/src/components/templates/AuthLayout.tsx @@ -2,6 +2,8 @@ import React from 'react' import styled, { keyframes } from 'styled-components' import { Logo } from '../atoms/Logo' import { PrivacyNote } from '../atoms/PrivacyNote' +import { Outlet, useLocation } from 'react-router-dom' +import { AUTH_ROUTES, AUTH_SUBTITLES } from '../../routes/authRoutes' const fadeIn = keyframes` from { opacity: 0; transform: translateY(16px); } @@ -10,7 +12,6 @@ const fadeIn = keyframes` const Page = styled.div` min-height: 100vh; - background: ${({ theme }) => theme.colors.bg}; display: flex; flex-direction: column; align-items: center; @@ -62,19 +63,21 @@ const Footer = styled.div` min-width:360px; ` -interface AuthLayoutProps { - subtitle?: string - children: React.ReactNode -} +export const AuthLayout: React.FC = () => { + const { pathname } = useLocation() + const subtitle = + AUTH_SUBTITLES[pathname as keyof typeof AUTH_SUBTITLES] ?? + AUTH_SUBTITLES[AUTH_ROUTES.base] -export const AuthLayout: React.FC = ({ subtitle, children }) => ( - - - - {children} -
- -
-
-
-) + return ( + + + + +
+ +
+
+
+ ) +} diff --git a/web/src/context/AuthContext.tsx b/web/src/context/AuthContext.tsx index e88b688..8a03b99 100644 --- a/web/src/context/AuthContext.tsx +++ b/web/src/context/AuthContext.tsx @@ -5,7 +5,7 @@ import { createUserWithEmailAndPassword, signInWithPopup, signOut, - type FirebaseError, + type FirebaseError } from 'firebase/auth' import { auth, googleProvider } from '../lib/firebase/firebase' import type { User, AuthContextType } from '../types' diff --git a/web/src/guards/RouteGuards.tsx b/web/src/guards/RouteGuards.tsx index 8bdd17e..c5d57e2 100644 --- a/web/src/guards/RouteGuards.tsx +++ b/web/src/guards/RouteGuards.tsx @@ -1,26 +1,27 @@ import React from 'react' -import { Navigate } from 'react-router-dom' +import { Navigate, Outlet } from 'react-router-dom' import { useAuth } from '../context/AuthContext' -import { LoadingScreen } from '../atoms/LoadingScreen' +import { LoadingScreen } from '../components/atoms/LoadingScreen' +import { AUTH_ROUTES } from '../routes/authRoutes' // ─── Protected Route ───────────────────────────────────────── // Redirects unauthenticated users to /auth -export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { +export const ProtectedRoute: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const { user, loading } = useAuth() if (loading) return - if (!user) return + if (!user) return - return <>{children} + return children ? <>{children} : } // ─── Public Route ───────────────────────────────────────────── // Redirects authenticated users away from auth pages to /dashboard -export const PublicRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { +export const PublicRoute: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const { user, loading } = useAuth() if (loading) return if (user) return - return <>{children} + return children ? <>{children} : } diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..0a81eb1 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,29 @@ +:root { + --color-primary: rgb(167, 34, 34); + --color-primary-container: rgb(167, 34, 34); + --color-on-primary: white; + --color-secondary: rgb(94, 25, 25); + --color-secondary-container: rgb(94, 25, 25); + --color-on-secondary: white; + --color-surface: rgb(20, 20, 20); + --color-surface-container: rgb(30, 30, 30); + --color-surface-bright: rgb(40, 40, 40); + --color-on-surface: white; + --color-on-surface-variant: rgb(100, 100, 100); + --color-outline: rgb(100, 100, 100); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Sen", sans-serif; +} + +body { + background-color: var(--color-surface); + color: var(--color-on-surface); +} + + + diff --git a/web/src/main.tsx b/web/src/main.tsx index 3e1e2c4..a2ddd26 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client' import "@fontsource/sen/400.css"; import "@fontsource/sen/800.css"; import App from './App.tsx' +import './index.css' createRoot(document.getElementById('root')!).render( diff --git a/web/src/molecules/GoogleButton.tsx b/web/src/molecules/GoogleButton.tsx deleted file mode 100644 index 46f0852..0000000 --- a/web/src/molecules/GoogleButton.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react' -import { Button } from '../atoms/Button' - -const GoogleIcon: React.FC = () => ( - - - - - - -) - -interface GoogleButtonProps { - onClick: () => void - loading?: boolean -} - -export const GoogleButton: React.FC = ({ onClick, loading }) => ( - -) diff --git a/web/src/molecules/LoginForm.tsx b/web/src/molecules/LoginForm.tsx deleted file mode 100644 index 0102a8f..0000000 --- a/web/src/molecules/LoginForm.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useRef } from 'react' -import styled from 'styled-components' -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { z } from 'zod' -import { Input } from '../atoms/Input' -import { Button } from '../atoms/Button' -import { ErrorMessage } from '../atoms/ErrorMessage' -import { useAuth } from '../context/AuthContext' - -const schema = z.object({ - email: z.email('Enter a valid email'), - password: z.string().min(1, 'Password is required') -}) - -type LoginFields = z.infer - -const Form = styled.form` - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - width: 100%; - & > :last-child { - margin-top: 20px; - } -` - -interface LoginFormProps { - onSuccess?: () => void -} - -export const LoginForm: React.FC = ({ onSuccess }) => { - const { loginWithEmail, error, clearError } = useAuth() - - // Track whether user has attempted a failed submit - const hasFailedSubmit = useRef(false) - - const { - register, - handleSubmit, - formState: { errors, isSubmitting, isValid }, - } = useForm({ - resolver: zodResolver(schema), - mode: 'onChange', // only active after a failed submit - }) - - // Re-validate live only after user hit submit with bad data - // so the button can unlock as soon as fields are corrected - const isLockedOut = hasFailedSubmit.current && !isValid - - const onSubmit = async (data: LoginFields) => { - clearError() - try { - await loginWithEmail(data.email, data.password) - onSuccess?.() - } catch (err) { - // error handled by context - console.error('Login failed', err) - } - } - - const onError = () => { - // User tried to submit invalid data — now we lock until fixed - hasFailedSubmit.current = true - } - - return ( -
- {error && } - - - {errors.email && } - - - {errors.password && } - - - - ) -} \ No newline at end of file diff --git a/web/src/molecules/RegisterForm.tsx b/web/src/molecules/RegisterForm.tsx deleted file mode 100644 index e17c209..0000000 --- a/web/src/molecules/RegisterForm.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useRef } from 'react' -import styled from 'styled-components' -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { z } from 'zod' -import { Input } from '../atoms/Input' -import { Button } from '../atoms/Button' -import { ErrorMessage } from '../atoms/ErrorMessage' -import { useAuth } from '../context/AuthContext' - -const schema = z - .object({ - email: z.email('Enter a valid email'), - password: z.string().min(8, 'Password must be at least 8 characters'), - repeatPassword: z.string().min(1, 'Please confirm your password'), - }) - .refine((data) => data.password === data.repeatPassword, { - message: 'Passwords do not match', - path: ['repeatPassword'], // attach error to repeatPassword field - }) - -type RegisterFields = z.infer - -const Form = styled.form` - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - width: 100%; - & > :last-child { - margin-top: 16px; - } -` - -export const RegisterForm: React.FC = () => { - const { registerWithEmail, error, clearError } = useAuth() - const hasFailedSubmit = useRef(false) - - const { - register, - handleSubmit, - formState: { errors, isSubmitting, isValid }, - } = useForm({ - resolver: zodResolver(schema), - mode: 'onChange', - }) - - const isLockedOut = hasFailedSubmit.current && !isValid - - const onSubmit = async (data: RegisterFields) => { - clearError() - try { - await registerWithEmail(data.email, data.password) - } catch(err) { - console.error('Registration failed', err); - // handled by context - } - } - - const onError = () => { - hasFailedSubmit.current = true - } - - return ( -
- {error && } - - - {errors.email && } - - - {errors.password && } - - - {errors.repeatPassword && } - - - - ) -} \ No newline at end of file diff --git a/web/src/pages/AuthPage.tsx b/web/src/pages/AuthPage.tsx deleted file mode 100644 index 9d98077..0000000 --- a/web/src/pages/AuthPage.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { useState } from 'react' -import { AuthLayout } from '../templates/AuthLayout' -import { LandingCard } from '../organisms/LandingCard' -import { LoginCard } from '../organisms/LoginCard' -import { RegisterCard } from '../organisms/RegisterCard' -import type { AuthView } from '../types' - -const subtitleMap: Record = { - landing: 'Your key to riches.', - login: 'Login to your account', - register: 'Create an Account', -} - -export const AuthPage: React.FC = () => { - const [view, setView] = useState('landing') - - return ( - - {view === 'landing' && } - {view === 'login' && } - {view === 'register' && } - - ) -} diff --git a/web/src/routes/authRoutes.ts b/web/src/routes/authRoutes.ts new file mode 100644 index 0000000..a668cc3 --- /dev/null +++ b/web/src/routes/authRoutes.ts @@ -0,0 +1,16 @@ +export const AUTH_ROUTES = { + base: '/auth', + login: '/auth/login', + register: '/auth/register', +} as const + +export const AUTH_ROUTE_SEGMENTS = { + login: 'login', + register: 'register', +} as const + +export const AUTH_SUBTITLES: Record<(typeof AUTH_ROUTES)[keyof typeof AUTH_ROUTES], string> = { + [AUTH_ROUTES.base]: 'Your key to riches.', + [AUTH_ROUTES.login]: 'Login to your account', + [AUTH_ROUTES.register]: 'Create an Account', +} diff --git a/web/src/styled.d.ts b/web/src/styled.d.ts deleted file mode 100644 index c20cade..0000000 --- a/web/src/styled.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import 'styled-components' -import type { Theme } from './styles/theme' - -declare module 'styled-components' { - export interface DefaultTheme extends Theme {} -} diff --git a/web/src/styles/theme.ts b/web/src/styles/theme.ts deleted file mode 100644 index fb6bcc7..0000000 --- a/web/src/styles/theme.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { createGlobalStyle } from 'styled-components' - -export const theme = { - colors: { - bg: '#111111', - bgCard: '#1a1a1a', - bgInput: '#1E1E1E', - red: '#A72222', - redHover: 'rgb(94, 25, 25)', - - white: '#ffffff', - border: '#2a2a2a', - muted: '#646464', - overmuted:'#626262', - borderFocus: '#c0392b', - text: '#e8e8e8', - google: '#141414', - }, - fonts: { - display: "'Sen', sans-serif", - body: "'Sen', sans-serif", - }, - radii: { - sm: '6px', - md: '10px', - lg: '14px', - full: '999px', - }, - transitions: { - fast: '0.15s ease', - normal: '0.25s ease', - }, -} - -export type Theme = typeof theme - -export const GlobalStyles = createGlobalStyle<{ theme: Theme }>` - *, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; - } - - html, body, #root { - height: 100%; - width: 100%; - } - - body { - font-family: ${({ theme }) => theme.fonts.body}; - background: ${({ theme }) => theme.colors.bg}; - color: ${({ theme }) => theme.colors.text}; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - - a { - color: inherit; - text-decoration: none; - } - - button { - font-family: inherit; - cursor: pointer; - border: none; - outline: none; - background: none; - } - - input { - font-family: inherit; - outline: none; - border: none; - background: none; - } -` diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 4b2791c..6115dae 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -16,5 +16,3 @@ export interface AuthContextType { logout: () => Promise clearError: () => void } - -export type AuthView = 'landing' | 'login' | 'register' From acde5f8b0e9e1a1a5d29530f86f6b681d9d0c340 Mon Sep 17 00:00:00 2001 From: Mariam Hagras Date: Sat, 7 Mar 2026 07:07:04 +0200 Subject: [PATCH 3/7] using createBrowseRrouter,and seprated routes file --- web/src/App.tsx | 58 ++-------------------------------- web/src/guards/RouteGuards.tsx | 35 ++++++++++---------- web/src/routes/AppRouter.tsx | 0 web/src/routes/routes.tsx | 53 +++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 72 deletions(-) delete mode 100644 web/src/routes/AppRouter.tsx create mode 100644 web/src/routes/routes.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index ed03e72..eb09c29 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,62 +1,10 @@ -import React from 'react'; -import { - createBrowserRouter, - RouterProvider, - Navigate, -} from 'react-router-dom'; +import { RouterProvider } from 'react-router'; +import router from './routes/routes'; import { AuthProvider } from './context/AuthContext'; -import { ProtectedRoute, PublicRoute } from './guards/RouteGuards'; -import { AuthPage } from './components/pages/AuthPage'; -import { DashboardPage } from './components/pages/DashboardPage'; -import { LandingCard } from './components/organisms/LandingCard'; -import { LoginCard } from './components/organisms/LoginCard'; -import { RegisterCard } from './components/organisms/RegisterCard'; -import { AUTH_ROUTES, AUTH_ROUTE_SEGMENTS } from './routes/authRoutes'; - -const router = createBrowserRouter([ - { - path: AUTH_ROUTES.base, - element: , - children: [ - { - element: , - children: [ - { - index: true, - element: , - }, - { - path: AUTH_ROUTE_SEGMENTS.login, - element: , - }, - { - path: AUTH_ROUTE_SEGMENTS.register, - element: , - }, - ], - }, - ], - }, - { - path: '/dashboard', - element: , - children: [ - { - index: true, - element: , - }, - ], - }, - { - path: '*', - element: , - }, -]); - const App: React.FC = () => { return ( - + ; ); }; diff --git a/web/src/guards/RouteGuards.tsx b/web/src/guards/RouteGuards.tsx index c5d57e2..ab1bede 100644 --- a/web/src/guards/RouteGuards.tsx +++ b/web/src/guards/RouteGuards.tsx @@ -1,27 +1,28 @@ -import React from 'react' -import { Navigate, Outlet } from 'react-router-dom' -import { useAuth } from '../context/AuthContext' -import { LoadingScreen } from '../components/atoms/LoadingScreen' -import { AUTH_ROUTES } from '../routes/authRoutes' +import React from 'react'; +import { Navigate, Outlet } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { AUTH_ROUTES } from '../routes/authRoutes'; // ─── Protected Route ───────────────────────────────────────── // Redirects unauthenticated users to /auth -export const ProtectedRoute: React.FC<{ children?: React.ReactNode }> = ({ children }) => { - const { user, loading } = useAuth() +export const ProtectedRoute: React.FC<{ children?: React.ReactNode }> = ({ + children, +}) => { + const { user } = useAuth(); - if (loading) return - if (!user) return + if (!user) return ; - return children ? <>{children} : -} + return children ? <>{children} : ; +}; // ─── Public Route ───────────────────────────────────────────── // Redirects authenticated users away from auth pages to /dashboard -export const PublicRoute: React.FC<{ children?: React.ReactNode }> = ({ children }) => { - const { user, loading } = useAuth() +export const PublicRoute: React.FC<{ children?: React.ReactNode }> = ({ + children, +}) => { + const { user } = useAuth(); - if (loading) return - if (user) return + if (user) return ; - return children ? <>{children} : -} + return children ? <>{children} : ; +}; diff --git a/web/src/routes/AppRouter.tsx b/web/src/routes/AppRouter.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/web/src/routes/routes.tsx b/web/src/routes/routes.tsx new file mode 100644 index 0000000..070610c --- /dev/null +++ b/web/src/routes/routes.tsx @@ -0,0 +1,53 @@ +import { createBrowserRouter, Navigate } from "react-router"; +import { ProtectedRoute, PublicRoute } from "../guards/RouteGuards"; + +import { AuthPage } from "../components/pages/AuthPage"; +import { DashboardPage } from "../components/pages/DashboardPage"; + +import { LandingCard } from "../components/organisms/LandingCard"; +import { LoginCard } from "../components/organisms/LoginCard"; +import { RegisterCard } from "../components/organisms/RegisterCard"; + +import { AUTH_ROUTES, AUTH_ROUTE_SEGMENTS } from "./authRoutes"; + +const routes = createBrowserRouter([ + { + path: AUTH_ROUTES.base, + element: , + children: [ + { + element: , + children: [ + { + index: true, + element: , + }, + { + path: AUTH_ROUTE_SEGMENTS.login, + element: , + }, + { + path: AUTH_ROUTE_SEGMENTS.register, + element: , + }, + ], + }, + ], + }, + { + path: "/dashboard", + element: , + children: [ + { + index: true, + element: , + }, + ], + }, + { + path: "*", + element: , + }, +]); + +export default routes; \ No newline at end of file From df110582effbfceffe775a274a73f4e4ffa22d37 Mon Sep 17 00:00:00 2001 From: Mariam Hagras Date: Sat, 7 Mar 2026 09:15:00 +0200 Subject: [PATCH 4/7] fixing file arcticture & romoving publicRoute --- .../{templates => layouts}/AuthLayout.tsx | 0 web/src/components/molecules/LoginFields.tsx | 39 ++++ .../components/molecules/RegisterFields.tsx | 55 ++++++ web/src/components/molecules/RegisterForm.tsx | 180 ------------------ web/src/components/organisms/LoginCard.tsx | 5 +- web/src/components/organisms/LoginForm.tsx | 102 ++++++++++ web/src/components/organisms/RegisterCard.tsx | 4 +- web/src/components/organisms/RegisterForm.tsx | 120 ++++++++++++ web/src/components/pages/AuthPage.tsx | 2 +- web/src/guards/RouteGuards.tsx | 14 +- web/src/routes/routes.tsx | 29 ++- 11 files changed, 335 insertions(+), 215 deletions(-) rename web/src/components/{templates => layouts}/AuthLayout.tsx (100%) create mode 100644 web/src/components/molecules/LoginFields.tsx create mode 100644 web/src/components/molecules/RegisterFields.tsx delete mode 100644 web/src/components/molecules/RegisterForm.tsx create mode 100644 web/src/components/organisms/LoginForm.tsx create mode 100644 web/src/components/organisms/RegisterForm.tsx diff --git a/web/src/components/templates/AuthLayout.tsx b/web/src/components/layouts/AuthLayout.tsx similarity index 100% rename from web/src/components/templates/AuthLayout.tsx rename to web/src/components/layouts/AuthLayout.tsx diff --git a/web/src/components/molecules/LoginFields.tsx b/web/src/components/molecules/LoginFields.tsx new file mode 100644 index 0000000..3269f9b --- /dev/null +++ b/web/src/components/molecules/LoginFields.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { Input } from '../atoms/Input' +import { ErrorMessage } from '../atoms/ErrorMessage' + +interface LoginFieldsProps { + email: string + password: string + errors: { email?: string; password?: string } + onChange: (e: React.ChangeEvent) => void +} + +export const LoginFields: React.FC = ({ + email, + password, + errors, + onChange, +}) => ( + <> + + {errors.email && } + + + {errors.password && } + +) \ No newline at end of file diff --git a/web/src/components/molecules/RegisterFields.tsx b/web/src/components/molecules/RegisterFields.tsx new file mode 100644 index 0000000..2e8aa17 --- /dev/null +++ b/web/src/components/molecules/RegisterFields.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import { Input } from '../atoms/Input' +import { ErrorMessage } from '../atoms/ErrorMessage' + +interface RegisterFieldsProps { + email: string + password: string + repeatPassword: string + errors: { + email?: string + password?: string + repeatPassword?: string + } + onChange: (e: React.ChangeEvent) => void +} + +export const RegisterFields: React.FC = ({ + email, + password, + repeatPassword, + errors, + onChange, +}) => ( + <> + + {errors.email && } + + + {errors.password && } + + + {errors.repeatPassword && } + +) \ No newline at end of file diff --git a/web/src/components/molecules/RegisterForm.tsx b/web/src/components/molecules/RegisterForm.tsx deleted file mode 100644 index 9014a41..0000000 --- a/web/src/components/molecules/RegisterForm.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React, { useState } from 'react' -import styled from 'styled-components' -import { Input } from '../atoms/Input' -import { Button } from '../atoms/Button' -import { ErrorMessage } from '../atoms/ErrorMessage' -import { useAuth } from '../../context/AuthContext' - -const Form = styled.form` - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - width: 100%; - & > :last-child { - margin-top: 16px; - } -` - -interface RegisterFields { - email: string - password: string - repeatPassword: string -} - -export const RegisterForm: React.FC = () => { - const { registerWithEmail, error, clearError } = useAuth() - - const [form, setForm] = useState({ - email: '', - password: '', - repeatPassword: '', - }) - - const [errors, setErrors] = useState>({}) - const [isSubmitting, setIsSubmitting] = useState(false) - const [hasFailedSubmit, setHasFailedSubmit] = useState(false) - - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target - - setForm(prev => ({ - ...prev, - [name]: value, - })) - - if (hasFailedSubmit) { - validateField(name as keyof RegisterFields, value) - } - } - - const validateField = ( - field: keyof RegisterFields, - value: string - ) => { - let message = '' - - if (field === 'email') { - if (!value) message = 'Email is required' - else if (!/\S+@\S+\.\S+/.test(value)) - message = 'Enter a valid email' - } - - if (field === 'password') { - if (!value) message = 'Password is required' - else if (value.length < 8) - message = 'Password must be at least 8 characters' - } - - if (field === 'repeatPassword') { - if (!value) message = 'Please confirm your password' - else if (value !== form.password) - message = 'Passwords do not match' - } - - setErrors(prev => ({ - ...prev, - [field]: message, - })) - } - - const validateForm = () => { - const newErrors: Partial = {} - - if (!form.email) newErrors.email = 'Email is required' - else if (!/\S+@\S+\.\S+/.test(form.email)) - newErrors.email = 'Enter a valid email' - - if (!form.password) - newErrors.password = 'Password is required' - else if (form.password.length < 8) - newErrors.password = - 'Password must be at least 8 characters' - - if (!form.repeatPassword) - newErrors.repeatPassword = - 'Please confirm your password' - else if (form.password !== form.repeatPassword) - newErrors.repeatPassword = - 'Passwords do not match' - - setErrors(newErrors) - - return Object.keys(newErrors).length === 0 - } - - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault() - clearError() - - const isValid = validateForm() - - if (!isValid) { - setHasFailedSubmit(true) - return - } - - try { - setIsSubmitting(true) - await registerWithEmail(form.email, form.password) - } catch (err) { - console.error('Registration failed', err) - } finally { - setIsSubmitting(false) - } - } - - const isLockedOut = - hasFailedSubmit && - (!!errors.email || - !!errors.password || - !!errors.repeatPassword) - - return ( -
- {error && } - - - {errors.email && } - - - {errors.password && } - - - {errors.repeatPassword && ( - - )} - - - - ) -} \ No newline at end of file diff --git a/web/src/components/organisms/LoginCard.tsx b/web/src/components/organisms/LoginCard.tsx index a98f126..172606a 100644 --- a/web/src/components/organisms/LoginCard.tsx +++ b/web/src/components/organisms/LoginCard.tsx @@ -1,10 +1,11 @@ import React from 'react' import styled from 'styled-components' -import { LoginForm } from '../molecules/LoginForm' +import { LoginForm } from './LoginForm' const Wrapper = styled.div` display: flex; flex-direction: column; + gap: 16px; width: 100%; ` @@ -12,4 +13,4 @@ export const LoginCard: React.FC = () => ( -) +) \ No newline at end of file diff --git a/web/src/components/organisms/LoginForm.tsx b/web/src/components/organisms/LoginForm.tsx new file mode 100644 index 0000000..4fcc9fc --- /dev/null +++ b/web/src/components/organisms/LoginForm.tsx @@ -0,0 +1,102 @@ +import React, { useState } from 'react' +import styled from 'styled-components' +import { LoginFields } from '../molecules/LoginFields' +import { Button } from '../atoms/Button' +import { ErrorMessage } from '../atoms/ErrorMessage' +import { useAuth } from '../../context/AuthContext' + +const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + width: 100%; + & > :last-child { + margin-top: 20px; + } +` + +interface LoginFormProps { + onSuccess?: () => void +} + +interface LoginFieldsState { + email: string + password: string +} + +export const LoginForm: React.FC = ({ onSuccess }) => { + const { loginWithEmail, error, clearError } = useAuth() + + const [form, setForm] = useState({ email: '', password: '' }) + const [errors, setErrors] = useState>({}) + const [isSubmitting, setIsSubmitting] = useState(false) + const [hasFailedSubmit, setHasFailedSubmit] = useState(false) + + // Handle input change + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setForm(prev => ({ ...prev, [name]: value })) + if (hasFailedSubmit) validateField(name as keyof LoginFieldsState, value) + } + + const validateField = (field: keyof LoginFieldsState, value: string) => { + let message = '' + if (field === 'email') { + if (!value) message = 'Email is required' + else if (!/\S+@\S+\.\S+/.test(value)) message = 'Enter a valid email' + } + if (field === 'password') { + if (!value) message = 'Password is required' + } + setErrors(prev => ({ ...prev, [field]: message })) + } + + const validateForm = () => { + const newErrors: Partial = {} + if (!form.email) newErrors.email = 'Email is required' + else if (!/\S+@\S+\.\S+/.test(form.email)) newErrors.email = 'Enter a valid email' + if (!form.password) newErrors.password = 'Password is required' + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault() + clearError() + const isValid = validateForm() + if (!isValid) { + setHasFailedSubmit(true) + return + } + try { + setIsSubmitting(true) + await loginWithEmail(form.email, form.password) + onSuccess?.() + } catch (err) { + console.error('Login failed', err) + } finally { + setIsSubmitting(false) + } + } + + const isLockedOut = hasFailedSubmit && (!!errors.email || !!errors.password) + + return ( +
+ {error && } + + + + + + ) +} \ No newline at end of file diff --git a/web/src/components/organisms/RegisterCard.tsx b/web/src/components/organisms/RegisterCard.tsx index ed7b643..dc6f633 100644 --- a/web/src/components/organisms/RegisterCard.tsx +++ b/web/src/components/organisms/RegisterCard.tsx @@ -1,6 +1,6 @@ import React from 'react' import styled from 'styled-components' -import { RegisterForm } from '../molecules/RegisterForm' +import { RegisterForm } from './RegisterForm' const Wrapper = styled.div` display: flex; @@ -13,4 +13,4 @@ export const RegisterCard: React.FC = () => ( -) +) \ No newline at end of file diff --git a/web/src/components/organisms/RegisterForm.tsx b/web/src/components/organisms/RegisterForm.tsx new file mode 100644 index 0000000..656e05e --- /dev/null +++ b/web/src/components/organisms/RegisterForm.tsx @@ -0,0 +1,120 @@ +import React, { useState } from 'react' +import styled from 'styled-components' +import { RegisterFields } from '../molecules/RegisterFields' +import { Button } from '../atoms/Button' +import { ErrorMessage } from '../atoms/ErrorMessage' +import { useAuth } from '../../context/AuthContext' + +const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + width: 100%; + & > :last-child { + margin-top: 16px; + } +` + +interface RegisterFieldsState { + email: string + password: string + repeatPassword: string +} + +export const RegisterForm: React.FC = () => { + const { registerWithEmail, error, clearError } = useAuth() + + const [form, setForm] = useState({ + email: '', + password: '', + repeatPassword: '', + }) + + const [errors, setErrors] = useState>({}) + const [isSubmitting, setIsSubmitting] = useState(false) + const [hasFailedSubmit, setHasFailedSubmit] = useState(false) + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setForm(prev => ({ ...prev, [name]: value })) + if (hasFailedSubmit) validateField(name as keyof RegisterFieldsState, value) + } + + const validateField = (field: keyof RegisterFieldsState, value: string) => { + let message = '' + if (field === 'email') { + if (!value) message = 'Email is required' + else if (!/\S+@\S+\.\S+/.test(value)) message = 'Enter a valid email' + } + if (field === 'password') { + if (!value) message = 'Password is required' + else if (value.length < 8) message = 'Password must be at least 8 characters' + } + if (field === 'repeatPassword') { + if (!value) message = 'Please confirm your password' + else if (value !== form.password) message = 'Passwords do not match' + } + setErrors(prev => ({ ...prev, [field]: message })) + } + + const validateForm = () => { + const newErrors: Partial = {} + if (!form.email) newErrors.email = 'Email is required' + else if (!/\S+@\S+\.\S+/.test(form.email)) newErrors.email = 'Enter a valid email' + + if (!form.password) newErrors.password = 'Password is required' + else if (form.password.length < 8) newErrors.password = 'Password must be at least 8 characters' + + if (!form.repeatPassword) newErrors.repeatPassword = 'Please confirm your password' + else if (form.password !== form.repeatPassword) newErrors.repeatPassword = 'Passwords do not match' + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault() + clearError() + const isValid = validateForm() + if (!isValid) { + setHasFailedSubmit(true) + return + } + try { + setIsSubmitting(true) + await registerWithEmail(form.email, form.password) + } catch (err) { + console.error('Registration failed', err) + } finally { + setIsSubmitting(false) + } + } + + const isLockedOut = + hasFailedSubmit && (!!errors.email || !!errors.password || !!errors.repeatPassword) + + return ( +
+ {error && } + + + + + + ) +} \ No newline at end of file diff --git a/web/src/components/pages/AuthPage.tsx b/web/src/components/pages/AuthPage.tsx index 730fa7c..fdbb62c 100644 --- a/web/src/components/pages/AuthPage.tsx +++ b/web/src/components/pages/AuthPage.tsx @@ -1,4 +1,4 @@ import React from 'react' -import { AuthLayout } from '../templates/AuthLayout' +import { AuthLayout } from '../layouts/AuthLayout' export const AuthPage: React.FC = () => diff --git a/web/src/guards/RouteGuards.tsx b/web/src/guards/RouteGuards.tsx index ab1bede..254e394 100644 --- a/web/src/guards/RouteGuards.tsx +++ b/web/src/guards/RouteGuards.tsx @@ -3,8 +3,7 @@ import { Navigate, Outlet } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import { AUTH_ROUTES } from '../routes/authRoutes'; -// ─── Protected Route ───────────────────────────────────────── -// Redirects unauthenticated users to /auth + export const ProtectedRoute: React.FC<{ children?: React.ReactNode }> = ({ children, }) => { @@ -15,14 +14,3 @@ export const ProtectedRoute: React.FC<{ children?: React.ReactNode }> = ({ return children ? <>{children} : ; }; -// ─── Public Route ───────────────────────────────────────────── -// Redirects authenticated users away from auth pages to /dashboard -export const PublicRoute: React.FC<{ children?: React.ReactNode }> = ({ - children, -}) => { - const { user } = useAuth(); - - if (user) return ; - - return children ? <>{children} : ; -}; diff --git a/web/src/routes/routes.tsx b/web/src/routes/routes.tsx index 070610c..e96bbe3 100644 --- a/web/src/routes/routes.tsx +++ b/web/src/routes/routes.tsx @@ -1,5 +1,5 @@ import { createBrowserRouter, Navigate } from "react-router"; -import { ProtectedRoute, PublicRoute } from "../guards/RouteGuards"; +import { ProtectedRoute } from "../guards/RouteGuards"; import { AuthPage } from "../components/pages/AuthPage"; import { DashboardPage } from "../components/pages/DashboardPage"; @@ -13,24 +13,19 @@ import { AUTH_ROUTES, AUTH_ROUTE_SEGMENTS } from "./authRoutes"; const routes = createBrowserRouter([ { path: AUTH_ROUTES.base, - element: , + element: , children: [ { - element: , - children: [ - { - index: true, - element: , - }, - { - path: AUTH_ROUTE_SEGMENTS.login, - element: , - }, - { - path: AUTH_ROUTE_SEGMENTS.register, - element: , - }, - ], + index: true, + element: , + }, + { + path: AUTH_ROUTE_SEGMENTS.login, + element: , + }, + { + path: AUTH_ROUTE_SEGMENTS.register, + element: , }, ], }, From b974d6308d4979a6d49b9dab4f06818778937616 Mon Sep 17 00:00:00 2001 From: Mariam Hagras Date: Sat, 7 Mar 2026 10:04:02 +0200 Subject: [PATCH 5/7] official google logo --- web/src/components/atoms/GoogleButton.tsx | 44 ++++++++++++----------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/web/src/components/atoms/GoogleButton.tsx b/web/src/components/atoms/GoogleButton.tsx index 7c28e5a..4ed3cdc 100644 --- a/web/src/components/atoms/GoogleButton.tsx +++ b/web/src/components/atoms/GoogleButton.tsx @@ -1,37 +1,41 @@ import React from 'react'; import { Button } from './Button'; -const GoogleIcon: React.FC = () => ( +interface GoogleButtonProps { + onClick: () => void; + loading?: boolean; +} + +const GoogleIcon = () => ( + 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" + > + 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" + > + 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" + > + 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" + > + ); -interface GoogleButtonProps { - onClick: () => void; - loading?: boolean; -} - export const GoogleButton: React.FC = ({ onClick, loading, From 8e2f166c149c5d73bf53df61063c59179f206954 Mon Sep 17 00:00:00 2001 From: Mariam Hagras Date: Sun, 8 Mar 2026 09:34:56 +0200 Subject: [PATCH 6/7] fixing completed --- web/src/components/atoms/Button.tsx | 6 +- web/src/components/atoms/ErrorMessage.tsx | 2 +- web/src/components/atoms/PrivacyNote.tsx | 5 +- web/src/components/molecules/LoginForm.tsx | 148 ----------- web/src/components/organisms/LandingCard.tsx | 13 +- web/src/components/organisms/LoginCard.tsx | 2 +- web/src/components/organisms/LoginForm.tsx | 188 +++++++------- web/src/components/organisms/RegisterCard.tsx | 21 +- web/src/components/organisms/RegisterForm.tsx | 243 ++++++++++-------- web/src/components/pages/DashboardPage.tsx | 7 +- web/src/context/AuthContext.tsx | 188 +++++++------- web/src/guards/RouteGuards.tsx | 8 +- web/src/lib/firebase/auth.ts | 59 +++++ web/src/lib/firebase/firebase.ts | 3 +- web/src/types/index.ts | 10 - 15 files changed, 412 insertions(+), 491 deletions(-) delete mode 100644 web/src/components/molecules/LoginForm.tsx create mode 100644 web/src/lib/firebase/auth.ts diff --git a/web/src/components/atoms/Button.tsx b/web/src/components/atoms/Button.tsx index 6b33027..c4f0c2b 100644 --- a/web/src/components/atoms/Button.tsx +++ b/web/src/components/atoms/Button.tsx @@ -38,7 +38,7 @@ const StyledButton = styled.button` background-color: var(--color-primary); color: var(--color-on-primary); &:not(:disabled):hover { - transform: translateY(-1px); + transform: translateY(-0.5px); } box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); `} @@ -51,7 +51,7 @@ const StyledButton = styled.button` box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); &:not(:disabled):hover { - transform: translateY(-1px); + transform: translateY(-0.5px); } `} @@ -63,7 +63,7 @@ const StyledButton = styled.button` color: var(--color-on-surface); &:not(:disabled):hover { - transform: translateY(-1px); + transform: translateY(-0.5px); } `} `; diff --git a/web/src/components/atoms/ErrorMessage.tsx b/web/src/components/atoms/ErrorMessage.tsx index d06dfb1..e81aedb 100644 --- a/web/src/components/atoms/ErrorMessage.tsx +++ b/web/src/components/atoms/ErrorMessage.tsx @@ -10,7 +10,7 @@ const Wrapper = styled.span` font-size: 0.75rem; color: var(--color-primary); animation: ${slideIn} 0.2s ease; - line-height: 0.5; + line-height: 0.2; text-align: center; `; diff --git a/web/src/components/atoms/PrivacyNote.tsx b/web/src/components/atoms/PrivacyNote.tsx index dba4bb8..0057932 100644 --- a/web/src/components/atoms/PrivacyNote.tsx +++ b/web/src/components/atoms/PrivacyNote.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Link } from 'react-router'; import styled from 'styled-components'; const Text = styled.p` @@ -17,8 +18,8 @@ const Text = styled.p` export const PrivacyNote: React.FC = () => ( By continuing, you agree to our{' '} - + Privacy Policy - + ); diff --git a/web/src/components/molecules/LoginForm.tsx b/web/src/components/molecules/LoginForm.tsx deleted file mode 100644 index 33a53a4..0000000 --- a/web/src/components/molecules/LoginForm.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import React, { useState } from 'react' -import styled from 'styled-components' -import { Input } from '../atoms/Input' -import { Button } from '../atoms/Button' -import { ErrorMessage } from '../atoms/ErrorMessage' -import { useAuth } from '../../context/AuthContext' - -const Form = styled.form` - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - width: 100%; - & > :last-child { - margin-top: 20px; - } -` - -interface LoginFormProps { - onSuccess?: () => void -} - -interface LoginFields { - email: string - password: string -} - -export const LoginForm: React.FC = ({ onSuccess }) => { - const { loginWithEmail, error, clearError } = useAuth() - - const [form, setForm] = useState({ - email: '', - password: '', - }) - - const [errors, setErrors] = useState>({}) - const [isSubmitting, setIsSubmitting] = useState(false) - const [hasFailedSubmit, setHasFailedSubmit] = useState(false) - - // Handle input change - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target - - setForm(prev => ({ - ...prev, - [name]: value, - })) - - // If user previously failed submit → revalidate live - if (hasFailedSubmit) { - validateField(name as keyof LoginFields, value) - } - } - - const validateField = (field: keyof LoginFields, value: string) => { - let message = '' - - if (field === 'email') { - if (!value) message = 'Email is required' - else if (!/\S+@\S+\.\S+/.test(value)) message = 'Enter a valid email' - } - - if (field === 'password') { - if (!value) message = 'Password is required' - } - - setErrors(prev => ({ - ...prev, - [field]: message, - })) - } - - const validateForm = () => { - const newErrors: Partial = {} - - if (!form.email) newErrors.email = 'Email is required' - else if (!/\S+@\S+\.\S+/.test(form.email)) - newErrors.email = 'Enter a valid email' - - if (!form.password) newErrors.password = 'Password is required' - - setErrors(newErrors) - - return Object.keys(newErrors).length === 0 - } - - const onSubmit = async (e: React.SubmitEvent) => { - e.preventDefault() - clearError() - - const isValid = validateForm() - - if (!isValid) { - setHasFailedSubmit(true) - return - } - - try { - setIsSubmitting(true) - await loginWithEmail(form.email, form.password) - onSuccess?.() - } catch (err) { - console.error('Login failed', err) - } finally { - setIsSubmitting(false) - } - } - - const isLockedOut = - hasFailedSubmit && - (!!errors.email || !!errors.password) - - return ( -
- {error && } - - - {errors.email && } - - - {errors.password && } - - - - ) -} \ No newline at end of file diff --git a/web/src/components/organisms/LandingCard.tsx b/web/src/components/organisms/LandingCard.tsx index c04474f..7eaa805 100644 --- a/web/src/components/organisms/LandingCard.tsx +++ b/web/src/components/organisms/LandingCard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import styled from 'styled-components' import { Button } from '../atoms/Button' import { Divider } from '../atoms/Divider' @@ -16,22 +16,27 @@ const Actions = styled.div` ` export const LandingCard: React.FC = () => { - const { loginWithGoogle, error, clearError } = useAuth() + const { googleLogin, error, clearError } = useAuth() const navigate = useNavigate() const [googleLoading, setGoogleLoading] = useState(false) + const{loading,user}=useAuth() const handleGoogle = async () => { try { setGoogleLoading(true) clearError() - await loginWithGoogle() + await googleLogin() } catch { // handled by context } finally { setGoogleLoading(false) } } - + useEffect(() => { + if (user && !loading) { + navigate("/dashboard"); + } +}, [user, loading]); return ( {error && } diff --git a/web/src/components/organisms/LoginCard.tsx b/web/src/components/organisms/LoginCard.tsx index 172606a..aa191d7 100644 --- a/web/src/components/organisms/LoginCard.tsx +++ b/web/src/components/organisms/LoginCard.tsx @@ -11,6 +11,6 @@ const Wrapper = styled.div` export const LoginCard: React.FC = () => ( - + ) \ No newline at end of file diff --git a/web/src/components/organisms/LoginForm.tsx b/web/src/components/organisms/LoginForm.tsx index 4fcc9fc..e764e7e 100644 --- a/web/src/components/organisms/LoginForm.tsx +++ b/web/src/components/organisms/LoginForm.tsx @@ -1,102 +1,116 @@ -import React, { useState } from 'react' -import styled from 'styled-components' -import { LoginFields } from '../molecules/LoginFields' -import { Button } from '../atoms/Button' -import { ErrorMessage } from '../atoms/ErrorMessage' -import { useAuth } from '../../context/AuthContext' +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { LoginFields } from '../molecules/LoginFields'; +import { Button } from '../atoms/Button'; +import { ErrorMessage } from '../atoms/ErrorMessage'; +import { useAuth } from '../../context/AuthContext'; +import { useNavigate } from 'react-router-dom'; const Form = styled.form` - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - width: 100%; - & > :last-child { - margin-top: 20px; - } -` - -interface LoginFormProps { - onSuccess?: () => void -} + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + width: 100%; + & > :last-child { + margin-top: 20px; + } +`; interface LoginFieldsState { - email: string - password: string + email: string; + password: string; } -export const LoginForm: React.FC = ({ onSuccess }) => { - const { loginWithEmail, error, clearError } = useAuth() +export const LoginForm: React.FC = () => { + const { login, error, clearError, user, loading } = useAuth(); + const navigate = useNavigate(); + const [form, setForm] = useState({ + email: '', + password: '', + }); + const [errors, setErrors] = useState>({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [hasFailedSubmit, setHasFailedSubmit] = useState(false); - const [form, setForm] = useState({ email: '', password: '' }) - const [errors, setErrors] = useState>({}) - const [isSubmitting, setIsSubmitting] = useState(false) - const [hasFailedSubmit, setHasFailedSubmit] = useState(false) + // Handle input change + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setForm((prev) => ({ ...prev, [name]: value })); + if (hasFailedSubmit) + validateField(name as keyof LoginFieldsState, value); + }; - // Handle input change - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target - setForm(prev => ({ ...prev, [name]: value })) - if (hasFailedSubmit) validateField(name as keyof LoginFieldsState, value) - } + const validateField = (field: keyof LoginFieldsState, value: string) => { + let message = ''; + if (field === 'email') { + if (!value) message = 'Email is required'; + else if (!/\S+@\S+\.\S+/.test(value)) + message = 'Enter a valid email'; + } + if (field === 'password') { + if (!value) message = 'Password is required'; + } + setErrors((prev) => ({ ...prev, [field]: message })); + }; - const validateField = (field: keyof LoginFieldsState, value: string) => { - let message = '' - if (field === 'email') { - if (!value) message = 'Email is required' - else if (!/\S+@\S+\.\S+/.test(value)) message = 'Enter a valid email' - } - if (field === 'password') { - if (!value) message = 'Password is required' - } - setErrors(prev => ({ ...prev, [field]: message })) - } + const validateForm = () => { + const newErrors: Partial = {}; + if (!form.email) newErrors.email = 'Email is required'; + else if (!/\S+@\S+\.\S+/.test(form.email)) + newErrors.email = 'Enter a valid email'; + if (!form.password) newErrors.password = 'Password is required'; + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; - const validateForm = () => { - const newErrors: Partial = {} - if (!form.email) newErrors.email = 'Email is required' - else if (!/\S+@\S+\.\S+/.test(form.email)) newErrors.email = 'Enter a valid email' - if (!form.password) newErrors.password = 'Password is required' - setErrors(newErrors) - return Object.keys(newErrors).length === 0 - } + const onSubmit = async (e: React.SubmitEvent) => { + e.preventDefault(); + clearError(); + const isValid = validateForm(); + if (!isValid) { + setHasFailedSubmit(true); + return; + } + try { + setIsSubmitting(true); + await login(form.email, form.password); + } catch (err) { + console.error('Login failed', err); + } finally { + setIsSubmitting(false); + } + }; - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault() - clearError() - const isValid = validateForm() - if (!isValid) { - setHasFailedSubmit(true) - return - } - try { - setIsSubmitting(true) - await loginWithEmail(form.email, form.password) - onSuccess?.() - } catch (err) { - console.error('Login failed', err) - } finally { - setIsSubmitting(false) - } - } + const isLockedOut = + hasFailedSubmit && (!!errors.email || !!errors.password); - const isLockedOut = hasFailedSubmit && (!!errors.email || !!errors.password) + useEffect(() => { + if (user && !loading) { + navigate('/dashboard'); + } + }, [user, loading]); - return ( -
- {error && } + return ( + + {error && } - + - - - ) -} \ No newline at end of file + + + ); +}; diff --git a/web/src/components/organisms/RegisterCard.tsx b/web/src/components/organisms/RegisterCard.tsx index dc6f633..91c7b20 100644 --- a/web/src/components/organisms/RegisterCard.tsx +++ b/web/src/components/organisms/RegisterCard.tsx @@ -1,16 +1,17 @@ -import React from 'react' -import styled from 'styled-components' -import { RegisterForm } from './RegisterForm' - +import React from "react"; +import styled from "styled-components"; +import { RegisterForm } from "./RegisterForm"; const Wrapper = styled.div` display: flex; flex-direction: column; gap: 16px; width: 100%; -` +`; -export const RegisterCard: React.FC = () => ( - - - -) \ No newline at end of file +export const RegisterCard: React.FC = () => { + return ( + + + + ); +}; \ No newline at end of file diff --git a/web/src/components/organisms/RegisterForm.tsx b/web/src/components/organisms/RegisterForm.tsx index 656e05e..5ba730a 100644 --- a/web/src/components/organisms/RegisterForm.tsx +++ b/web/src/components/organisms/RegisterForm.tsx @@ -1,120 +1,137 @@ -import React, { useState } from 'react' -import styled from 'styled-components' -import { RegisterFields } from '../molecules/RegisterFields' -import { Button } from '../atoms/Button' -import { ErrorMessage } from '../atoms/ErrorMessage' -import { useAuth } from '../../context/AuthContext' +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { RegisterFields } from '../molecules/RegisterFields'; +import { Button } from '../atoms/Button'; +import { ErrorMessage } from '../atoms/ErrorMessage'; +import { useAuth } from '../../context/AuthContext'; +import { useNavigate } from 'react-router-dom'; const Form = styled.form` - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - width: 100%; - & > :last-child { - margin-top: 16px; - } -` + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + width: 100%; + & > :last-child { + margin-top: 16px; + } +`; interface RegisterFieldsState { - email: string - password: string - repeatPassword: string + email: string; + password: string; + repeatPassword: string; } export const RegisterForm: React.FC = () => { - const { registerWithEmail, error, clearError } = useAuth() - - const [form, setForm] = useState({ - email: '', - password: '', - repeatPassword: '', - }) - - const [errors, setErrors] = useState>({}) - const [isSubmitting, setIsSubmitting] = useState(false) - const [hasFailedSubmit, setHasFailedSubmit] = useState(false) - - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target - setForm(prev => ({ ...prev, [name]: value })) - if (hasFailedSubmit) validateField(name as keyof RegisterFieldsState, value) - } - - const validateField = (field: keyof RegisterFieldsState, value: string) => { - let message = '' - if (field === 'email') { - if (!value) message = 'Email is required' - else if (!/\S+@\S+\.\S+/.test(value)) message = 'Enter a valid email' - } - if (field === 'password') { - if (!value) message = 'Password is required' - else if (value.length < 8) message = 'Password must be at least 8 characters' - } - if (field === 'repeatPassword') { - if (!value) message = 'Please confirm your password' - else if (value !== form.password) message = 'Passwords do not match' - } - setErrors(prev => ({ ...prev, [field]: message })) - } - - const validateForm = () => { - const newErrors: Partial = {} - if (!form.email) newErrors.email = 'Email is required' - else if (!/\S+@\S+\.\S+/.test(form.email)) newErrors.email = 'Enter a valid email' - - if (!form.password) newErrors.password = 'Password is required' - else if (form.password.length < 8) newErrors.password = 'Password must be at least 8 characters' - - if (!form.repeatPassword) newErrors.repeatPassword = 'Please confirm your password' - else if (form.password !== form.repeatPassword) newErrors.repeatPassword = 'Passwords do not match' - - setErrors(newErrors) - return Object.keys(newErrors).length === 0 - } - - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault() - clearError() - const isValid = validateForm() - if (!isValid) { - setHasFailedSubmit(true) - return - } - try { - setIsSubmitting(true) - await registerWithEmail(form.email, form.password) - } catch (err) { - console.error('Registration failed', err) - } finally { - setIsSubmitting(false) - } - } - - const isLockedOut = - hasFailedSubmit && (!!errors.email || !!errors.password || !!errors.repeatPassword) - - return ( -
- {error && } - - - - - - ) -} \ No newline at end of file + const { register, error, clearError, user, loading } = useAuth(); + + const [form, setForm] = useState({ + email: '', + password: '', + repeatPassword: '', + }); + + const [errors, setErrors] = useState>({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [hasFailedSubmit, setHasFailedSubmit] = useState(false); + const navigate = useNavigate(); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setForm((prev) => ({ ...prev, [name]: value })); + if (hasFailedSubmit) + validateField(name as keyof RegisterFieldsState, value); + }; + + const validateField = (field: keyof RegisterFieldsState, value: string) => { + let message = ''; + if (field === 'email') { + if (!value) message = 'Email is required'; + else if (!/\S+@\S+\.\S+/.test(value)) + message = 'Enter a valid email'; + } + if (field === 'password') { + if (!value) message = 'Password is required'; + else if (value.length < 8) + message = 'Password must be at least 8 characters'; + } + if (field === 'repeatPassword') { + if (!value) message = 'Please confirm your password'; + else if (value !== form.password) + message = 'Passwords do not match'; + } + setErrors((prev) => ({ ...prev, [field]: message })); + }; + + const validateForm = () => { + const newErrors: Partial = {}; + if (!form.email) newErrors.email = 'Email is required'; + else if (!/\S+@\S+\.\S+/.test(form.email)) + newErrors.email = 'Enter a valid email'; + + if (!form.password) newErrors.password = 'Password is required'; + else if (form.password.length < 8) + newErrors.password = 'Password must be at least 8 characters'; + + if (!form.repeatPassword) + newErrors.repeatPassword = 'Please confirm your password'; + else if (form.password !== form.repeatPassword) + newErrors.repeatPassword = 'Passwords do not match'; + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const onSubmit = async (e: React.SubmitEvent) => { + e.preventDefault(); + clearError(); + const isValid = validateForm(); + if (!isValid) { + setHasFailedSubmit(true); + return; + } + try { + setIsSubmitting(true); + await register(form.email, form.password); + } catch (err) { + console.error('Registration failed', err); + } finally { + setIsSubmitting(false); + } + }; + + const isLockedOut = + hasFailedSubmit && + (!!errors.email || !!errors.password || !!errors.repeatPassword); + + useEffect(() => { + if (user && !loading) { + navigate('/dashboard'); + } + }, [user, loading]); + + return ( +
+ {error && } + + + + + + ); +}; diff --git a/web/src/components/pages/DashboardPage.tsx b/web/src/components/pages/DashboardPage.tsx index b5bfc9e..667bbab 100644 --- a/web/src/components/pages/DashboardPage.tsx +++ b/web/src/components/pages/DashboardPage.tsx @@ -14,14 +14,11 @@ const Page = styled.div` ` const Title = styled.h1` - font-size: 3rem; - letter-spacing: 0.04em; ` const Subtitle = styled.p` - font-size: 1rem; ` @@ -36,7 +33,7 @@ const Badge = styled.span` ` export const DashboardPage: React.FC = () => { - const { user, logout } = useAuth() + const { user, signOut } = useAuth() return ( @@ -45,7 +42,7 @@ export const DashboardPage: React.FC = () => { Welcome, {user?.email ?? 'User'} - diff --git a/web/src/context/AuthContext.tsx b/web/src/context/AuthContext.tsx index 8a03b99..3c691a2 100644 --- a/web/src/context/AuthContext.tsx +++ b/web/src/context/AuthContext.tsx @@ -1,109 +1,97 @@ -import React, { createContext, useContext, useEffect, useState, useCallback } from 'react' +import React, { createContext, useContext, useEffect, useState } from 'react'; import { - onAuthStateChanged, - signInWithEmailAndPassword, - createUserWithEmailAndPassword, - signInWithPopup, - signOut, - type FirebaseError -} from 'firebase/auth' -import { auth, googleProvider } from '../lib/firebase/firebase' -import type { User, AuthContextType } from '../types' + loginWithEmail, + registerWithEmail, + loginWithGoogle, + logout, +} from '../lib/firebase/auth'; +import { onAuthStateChanged } from 'firebase/auth'; +import { auth } from '../lib/firebase/firebase'; +import type { User } from '../types'; -const AuthContext = createContext(null) +const AuthContext = createContext(null); +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => { + if (firebaseUser) { + setUser({ + uid: firebaseUser.uid, + email: firebaseUser.email, + displayName: firebaseUser.displayName, + photoURL: firebaseUser.photoURL, + emailVerified: firebaseUser.emailVerified, + }); + } else { + setUser(null); + } + setLoading(false); + }); -function mapFirebaseError(error: FirebaseError): string { - switch (error.code) { - case 'auth/user-not-found': - case 'auth/wrong-password': - case 'auth/invalid-credential': - return 'Invalid email or password.' - case 'auth/email-already-in-use': - return 'This email is already registered.' - case 'auth/weak-password': - return 'Password must be at least 6 characters.' - case 'auth/invalid-email': - return 'Please enter a valid email address.' - case 'auth/too-many-requests': - return 'Too many attempts. Please try again later.' - case 'auth/popup-closed-by-user': - return 'Google sign-in was cancelled.' - default: - return 'An unexpected error occurred. Please try again.' - } -} + return () => unsubscribe(); + }, []); -export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [user, setUser] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) + const clearError = () => setError(null); + const login = async (email: string, password: string) => { + try { + setError(null); + await loginWithEmail(email, password); + } catch (err: any) { + setError(err.message); + } + }; - useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => { - if (firebaseUser) { - setUser({ - uid: firebaseUser.uid, - email: firebaseUser.email, - displayName: firebaseUser.displayName, - photoURL: firebaseUser.photoURL, - emailVerified: firebaseUser.emailVerified, - }) - } else { - setUser(null) - } - setLoading(false) - }) + const register = async (email: string, password: string) => { + try { + setError(null); + await registerWithEmail(email, password); + } catch (err: any) { + setError(err.message); + } + }; - return () => unsubscribe() - }, []) + const googleLogin = async () => { + try { + setError(null); + await loginWithGoogle(); + } catch (err: any) { + setError(err.message); + } + }; - const clearError = useCallback(() => setError(null), []) + const signOut = async () => { + try { + setError(null); + await logout(); + } catch (err: any) { + setError(err.message); + } + }; - const loginWithEmail = useCallback(async (email: string, password: string) => { - try { - setError(null) - await signInWithEmailAndPassword(auth, email, password) - } catch (err) { - setError(mapFirebaseError(err as FirebaseError)) - throw err - } - }, []) + return ( + + {children} + + ); +}; - const registerWithEmail = useCallback(async (email: string, password: string) => { - try { - setError(null) - await createUserWithEmailAndPassword(auth, email, password) - } catch (err) { - setError(mapFirebaseError(err as FirebaseError)) - throw err - } - }, []) - - const loginWithGoogle = useCallback(async () => { - try { - setError(null) - await signInWithPopup(auth, googleProvider) - } catch (err) { - setError(mapFirebaseError(err as FirebaseError)) - throw err - } - }, []) - - const logout = useCallback(async () => { - await signOut(auth) - }, []) - - return ( - - {children} - - ) -} - -export const useAuth = (): AuthContextType => { - const ctx = useContext(AuthContext) - if (!ctx) throw new Error('useAuth must be used inside AuthProvider') - return ctx -} +export const useAuth = (): any => { + const ctx = useContext(AuthContext); + return ctx; +}; diff --git a/web/src/guards/RouteGuards.tsx b/web/src/guards/RouteGuards.tsx index 254e394..736efff 100644 --- a/web/src/guards/RouteGuards.tsx +++ b/web/src/guards/RouteGuards.tsx @@ -4,13 +4,11 @@ import { useAuth } from '../context/AuthContext'; import { AUTH_ROUTES } from '../routes/authRoutes'; -export const ProtectedRoute: React.FC<{ children?: React.ReactNode }> = ({ - children, -}) => { +export const ProtectedRoute: React.FC<{ children?: React.ReactNode }> = () => { const { user } = useAuth(); - if (!user) return ; + if (!user) return - return children ? <>{children} : ; + return ; }; diff --git a/web/src/lib/firebase/auth.ts b/web/src/lib/firebase/auth.ts new file mode 100644 index 0000000..6d3ef11 --- /dev/null +++ b/web/src/lib/firebase/auth.ts @@ -0,0 +1,59 @@ +import { auth } from "./firebase"; +import { + createUserWithEmailAndPassword, + signInWithEmailAndPassword, + GoogleAuthProvider, + signInWithPopup, + signOut, + type AuthError, +} from "firebase/auth"; + +function mapFirebaseError(error: AuthError): string { + switch (error.code) { + case "auth/user-not-found": + case "auth/wrong-password": + case "auth/invalid-credential": + return "Invalid email or password."; + case "auth/email-already-in-use": + return "This email is already registered."; + case "auth/weak-password": + return "Password must be at least 6 characters."; + case "auth/invalid-email": + return "Please enter a valid email address."; + case "auth/too-many-requests": + return "Too many attempts. Please try again later."; + case "auth/popup-closed-by-user": + return "Google sign-in was cancelled."; + default: + return "An unexpected error occurred."; + } +} + +export const registerWithEmail = async (email: string, password: string) => { + try { + return await createUserWithEmailAndPassword(auth, email, password); + } catch (err) { + throw new Error(mapFirebaseError(err as AuthError)); + } +}; + +export const loginWithEmail = async (email: string, password: string) => { + try { + return await signInWithEmailAndPassword(auth, email, password); + } catch (err) { + throw new Error(mapFirebaseError(err as AuthError)); + } +}; + +export const loginWithGoogle = async () => { + try { + const provider = new GoogleAuthProvider(); + return await signInWithPopup(auth, provider); + } catch (err) { + throw new Error(mapFirebaseError(err as AuthError)); + } +}; + +export const logout = async () => { + return await signOut(auth); +}; \ No newline at end of file diff --git a/web/src/lib/firebase/firebase.ts b/web/src/lib/firebase/firebase.ts index de8bee0..3c8c1e8 100644 --- a/web/src/lib/firebase/firebase.ts +++ b/web/src/lib/firebase/firebase.ts @@ -2,7 +2,6 @@ import { initializeApp } from "firebase/app"; import { getAnalytics } from "firebase/analytics"; import { connectAuthEmulator, getAuth } from "firebase/auth"; import { connectFirestoreEmulator, getFirestore } from "firebase/firestore"; -import { GoogleAuthProvider } from "firebase/auth"; const firebaseConfig = { apiKey: "AIzaSyDhXWp5E7LI2pUY61aJX2jDWlwoZUOQ_6Q", authDomain: "financial-planner-72109.firebaseapp.com", @@ -17,7 +16,7 @@ export const app = initializeApp(firebaseConfig); export const auth = getAuth(app); export const db = getFirestore(app); export const analytics = getAnalytics(app); -export const googleProvider = new GoogleAuthProvider(); + if(window.location.hostname === "localhost") { connectAuthEmulator(auth, "http://localhost:9099"); diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 6115dae..d29d9e5 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -6,13 +6,3 @@ export interface User { emailVerified: boolean } -export interface AuthContextType { - user: User | null - loading: boolean - error: string | null - loginWithEmail: (email: string, password: string) => Promise - registerWithEmail: (email: string, password: string) => Promise - loginWithGoogle: () => Promise - logout: () => Promise - clearError: () => void -} From cfc7e38faec35787998fea05fb5c45519338ae54 Mon Sep 17 00:00:00 2001 From: Mariam Hagras Date: Sun, 8 Mar 2026 09:56:20 +0200 Subject: [PATCH 7/7] fixing resposiveness --- web/src/components/atoms/Logo.tsx | 20 +++++++++----------- web/src/components/layouts/AuthLayout.tsx | 9 +++++++++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/web/src/components/atoms/Logo.tsx b/web/src/components/atoms/Logo.tsx index d6dc9b0..cd32ee1 100644 --- a/web/src/components/atoms/Logo.tsx +++ b/web/src/components/atoms/Logo.tsx @@ -1,23 +1,21 @@ 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; - - `; - - - + margin-bottom: 30px; + @media (max-width: 393px) { + margin-bottom: 15px; + } +`; const Title = styled.h1` font-size: 2.4rem; @@ -26,7 +24,7 @@ const Title = styled.h1` `; const Subtitle = styled.p` - font-size:20px; + font-size: 20px; letter-spacing: 0.02em; `; diff --git a/web/src/components/layouts/AuthLayout.tsx b/web/src/components/layouts/AuthLayout.tsx index 94e0093..731763f 100644 --- a/web/src/components/layouts/AuthLayout.tsx +++ b/web/src/components/layouts/AuthLayout.tsx @@ -34,6 +34,9 @@ const Page = styled.div` background: radial-gradient(circle, rgba(192, 57, 43, 0.06) 0%, transparent 70%); pointer-events: none; } + @media (max-width: 393px) { + padding-top: 57px; + } ` const Card = styled.div` @@ -44,6 +47,9 @@ const Card = styled.div` align-items: center; gap: 32px; animation: ${fadeIn} 0.4s ease; + @media (max-width: 393px) { + gap:16px; + } ` const ContentArea = styled.div` @@ -52,6 +58,9 @@ const ContentArea = styled.div` flex-direction: column; align-items: center; gap: 12px; + @media (max-width: 393px) { + gap: 6px; + } `