From f5ce02003151517f54c15a8bd4a8d35c85e79f22 Mon Sep 17 00:00:00 2001 From: Greg Brown Date: Fri, 10 May 2024 22:24:29 +0100 Subject: [PATCH] feat: add sign in and sign up with Firebase Auth --- apps/web-ui/next.config.mjs | 11 ++- apps/web-ui/package.json | 1 + .../src/app/(public)/auth/signIn/page.tsx | 32 +++++++ .../src/app/(public)/auth/signUp/page.tsx | 32 +++++++ apps/web-ui/src/app/globals.css | 15 +-- apps/web-ui/src/app/layout.tsx | 20 +++- apps/web-ui/src/app/page.tsx | 15 +-- apps/web-ui/src/components/AuthForm/index.tsx | 82 ++++++++++++++++ apps/web-ui/src/components/Link/index.tsx | 33 +++++++ apps/web-ui/src/components/Nav/index.tsx | 95 +++++++++++++++++++ apps/web-ui/src/contexts/firebase/index.ts | 23 +++++ apps/web-ui/src/types/either.ts | 7 ++ packages/config-eslint/next.js | 61 ++++++------ yarn.lock | 17 +++- 14 files changed, 387 insertions(+), 57 deletions(-) create mode 100644 apps/web-ui/src/app/(public)/auth/signIn/page.tsx create mode 100644 apps/web-ui/src/app/(public)/auth/signUp/page.tsx create mode 100644 apps/web-ui/src/components/AuthForm/index.tsx create mode 100644 apps/web-ui/src/components/Link/index.tsx create mode 100644 apps/web-ui/src/components/Nav/index.tsx create mode 100644 apps/web-ui/src/contexts/firebase/index.ts create mode 100644 apps/web-ui/src/types/either.ts diff --git a/apps/web-ui/next.config.mjs b/apps/web-ui/next.config.mjs index b26f86c..50ed057 100644 --- a/apps/web-ui/next.config.mjs +++ b/apps/web-ui/next.config.mjs @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - webpack: (config, { isServer }) => { + webpack: (config, {isServer}) => { if (!isServer) { } return config; @@ -8,6 +8,15 @@ const nextConfig = { experimental: { instrumentationHook: true, }, + images: { + // https://nextjs.org/docs/pages/api-reference/components/image#dangerouslyallowsvg + dangerouslyAllowSVG: true, + remotePatterns: [ + { + hostname: "tailwindui.com", + } + ] + }, }; export default nextConfig; diff --git a/apps/web-ui/package.json b/apps/web-ui/package.json index 5c7cfaa..c4b260d 100644 --- a/apps/web-ui/package.json +++ b/apps/web-ui/package.json @@ -23,6 +23,7 @@ "next": "14.2.3", "react": "^18", "react-dom": "^18", + "tailwind-merge": "^2.3.0", "urql": "^4.0.7" }, "devDependencies": { diff --git a/apps/web-ui/src/app/(public)/auth/signIn/page.tsx b/apps/web-ui/src/app/(public)/auth/signIn/page.tsx new file mode 100644 index 0000000..2820633 --- /dev/null +++ b/apps/web-ui/src/app/(public)/auth/signIn/page.tsx @@ -0,0 +1,32 @@ +"use client"; +import {ComponentProps, useCallback} from 'react'; +import { signInWithEmailAndPassword } from 'firebase/auth'; +import {auth} from "@/contexts/firebase"; +import AuthForm from "@/components/AuthForm"; +import {Either} from "@/types/either.ts"; +import {useRouter} from "next/navigation"; + +type AuthFormProps = ComponentProps; + +export default function Page() { + const router = useRouter(); + + const handleSubmit = useCallback(async (values) => { + try { + const {email, password} = values; + await signInWithEmailAndPassword(auth, email, password); + router.push("/"); + } catch (error: unknown) { + if (error instanceof Error) { + return Either.left(error.message); + } + return Either.left("Something went wrong"); + } + }, [router]); + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/web-ui/src/app/(public)/auth/signUp/page.tsx b/apps/web-ui/src/app/(public)/auth/signUp/page.tsx new file mode 100644 index 0000000..71753c8 --- /dev/null +++ b/apps/web-ui/src/app/(public)/auth/signUp/page.tsx @@ -0,0 +1,32 @@ +"use client"; +import {ComponentProps, useCallback} from 'react'; +import { createUserWithEmailAndPassword } from 'firebase/auth'; +import {auth} from "@/contexts/firebase"; +import AuthForm from "@/components/AuthForm"; +import {Either} from "@/types/either.ts"; +import { useRouter } from 'next/navigation'; + +type AuthFormProps = ComponentProps; + +export default function Page() { + const router = useRouter(); + + const handleSubmit = useCallback(async (values) => { + try { + const {email, password} = values; + await createUserWithEmailAndPassword(auth, email, password); + router.push("/"); + } catch (error: unknown) { + if (error instanceof Error) { + return Either.left(error.message); + } + return Either.left("Something went wrong"); + } + }, [router]); + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/web-ui/src/app/globals.css b/apps/web-ui/src/app/globals.css index 875c01e..650a2e5 100644 --- a/apps/web-ui/src/app/globals.css +++ b/apps/web-ui/src/app/globals.css @@ -8,22 +8,9 @@ --background-end-rgb: 255, 255, 255; } -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + } @layer utilities { diff --git a/apps/web-ui/src/app/layout.tsx b/apps/web-ui/src/app/layout.tsx index 4494983..82a1f2f 100644 --- a/apps/web-ui/src/app/layout.tsx +++ b/apps/web-ui/src/app/layout.tsx @@ -2,6 +2,9 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import { GraphqlProvider } from "@/contexts/graphql/provider"; +import React from "react"; +import Nav from "@/components/Nav"; +import {twMerge} from "tailwind-merge"; const inter = Inter({ subsets: ["latin"] }); @@ -16,10 +19,21 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const isLoggedIn = false; return ( - - - {children} + + + +
+
+
); diff --git a/apps/web-ui/src/app/page.tsx b/apps/web-ui/src/app/page.tsx index 4f19e5d..8e422f7 100644 --- a/apps/web-ui/src/app/page.tsx +++ b/apps/web-ui/src/app/page.tsx @@ -15,14 +15,15 @@ export default function Home() { } return ( -
+
-
-

- {result.data?.ping} -

- +
+
+

{result.data?.ping}

+
+
-
+ + ); } diff --git a/apps/web-ui/src/components/AuthForm/index.tsx b/apps/web-ui/src/components/AuthForm/index.tsx new file mode 100644 index 0000000..6619ed5 --- /dev/null +++ b/apps/web-ui/src/components/AuthForm/index.tsx @@ -0,0 +1,82 @@ +"use client"; +import React, {useState, type FormEvent} from 'react'; +import type {Either} from "@/types/either.ts"; + +interface Props { + type: 'signIn' | 'signUp'; + onSubmit: (values: FormValues) => Promise | void>; +} + +interface FormValues { + email: string; + password: string; +} + +const AuthForm: React.FC = ({type, onSubmit}) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + try { + const result = await onSubmit({ + email, + password, + }); + if (result && result.kind === 'left') { + setError(result.value); + } + } catch (error: unknown) { + setError("Something went wrong"); + } + }; + + return ( +
+
+ + { + setEmail(e.target.value); + }} + placeholder="Email" + required + type="email" + value={email} + /> +
+
+ + { + setPassword(e.target.value); + }} + placeholder="******************" + required + type="password" + value={password} + /> +
+ {error ?

{error}

: null} +
+ +
+
+ ); +} + +export default AuthForm; \ No newline at end of file diff --git a/apps/web-ui/src/components/Link/index.tsx b/apps/web-ui/src/components/Link/index.tsx new file mode 100644 index 0000000..78d0432 --- /dev/null +++ b/apps/web-ui/src/components/Link/index.tsx @@ -0,0 +1,33 @@ +import React, {ComponentProps} from "react"; +import {default as NextLink} from "next/link"; +import {twMerge} from "tailwind-merge"; + +type Props = ComponentProps & { + selected?: boolean; + disabled?: boolean; +} + +const Link: React.FC = ({children, selected, disabled, ...props}) => { + return ( + + {children} + + ) +} + +export default Link; \ No newline at end of file diff --git a/apps/web-ui/src/components/Nav/index.tsx b/apps/web-ui/src/components/Nav/index.tsx new file mode 100644 index 0000000..450fa8d --- /dev/null +++ b/apps/web-ui/src/components/Nav/index.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import Link from "@/components/Link" +import Image from "next/image"; + +// TODO: add sign in, signup, and signout links + +interface Props { + isLoggedIn?: boolean; +} + +const Nav: React.FC = ({isLoggedIn}) => { + return ( + + ) +} + +export default Nav; \ No newline at end of file diff --git a/apps/web-ui/src/contexts/firebase/index.ts b/apps/web-ui/src/contexts/firebase/index.ts new file mode 100644 index 0000000..14545e3 --- /dev/null +++ b/apps/web-ui/src/contexts/firebase/index.ts @@ -0,0 +1,23 @@ +import { initializeApp } from "firebase/app"; +import { getAnalytics, isSupported } from "firebase/analytics"; +import { getAuth, connectAuthEmulator } from "firebase/auth"; + +// https://firebase.google.com/docs/web/setup#available-libraries + +// TODO: put this in a .env file +const firebaseConfig = { + +}; + +const app = initializeApp(firebaseConfig); + +isSupported().then((isSupported) => { + if (isSupported) { + console.log("Firebase Analytics is supported"); + const analytics = getAnalytics(app); + // Use analytics. + } +}) + +export const auth = getAuth(); +connectAuthEmulator(auth, "http://127.0.0.1:9099"); \ No newline at end of file diff --git a/apps/web-ui/src/types/either.ts b/apps/web-ui/src/types/either.ts new file mode 100644 index 0000000..fa7f161 --- /dev/null +++ b/apps/web-ui/src/types/either.ts @@ -0,0 +1,7 @@ +type Either = { kind: 'left', value: L } | { kind: 'right', value: R }; +const Either = { + left: (value: L): Either => ({ kind: 'left', value }), + right: (value: R): Either => ({ kind: 'right', value }), +}; + +export { Either }; \ No newline at end of file diff --git a/packages/config-eslint/next.js b/packages/config-eslint/next.js index ffca495..21b63f3 100644 --- a/packages/config-eslint/next.js +++ b/packages/config-eslint/next.js @@ -11,33 +11,40 @@ const project = resolve(process.cwd(), "tsconfig.json"); * */ +// module.exports = { +// extends: [ +// "@vercel/style-guide/eslint/node", +// "@vercel/style-guide/eslint/typescript", +// "@vercel/style-guide/eslint/browser", +// "@vercel/style-guide/eslint/react", +// "@vercel/style-guide/eslint/next", +// "eslint-config-turbo", +// ].map(require.resolve), +// parserOptions: { +// project, +// }, +// globals: { +// React: true, +// JSX: true, +// }, +// plugins: ["only-warn"], +// settings: { +// "import/resolver": { +// typescript: { +// project, +// }, +// }, +// }, +// ignorePatterns: [".*.js", "node_modules/", "dist/"], +// // add rules configurations here +// rules: { +// "import/no-default-export": "off", +// }, +// }; + module.exports = { extends: [ - "@vercel/style-guide/eslint/node", - "@vercel/style-guide/eslint/typescript", - "@vercel/style-guide/eslint/browser", - "@vercel/style-guide/eslint/react", - "@vercel/style-guide/eslint/next", - "eslint-config-turbo", - ].map(require.resolve), - parserOptions: { - project, - }, - globals: { - React: true, - JSX: true, - }, - plugins: ["only-warn"], - settings: { - "import/resolver": { - typescript: { - project, - }, - }, - }, - ignorePatterns: [".*.js", "node_modules/", "dist/"], - // add rules configurations here - rules: { - "import/no-default-export": "off", - }, + "next/core-web-vitals", + "eslint-config-turbo" + ] }; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 26ebc37..da9b23a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -486,7 +486,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.23.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.1": version "7.24.5" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz" integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g== @@ -5244,10 +5244,10 @@ firebase-functions@^5.0.1: express "^4.17.1" protobufjs "^7.2.2" -firebase-tools@13.8.1: - version "13.8.1" - resolved "https://registry.yarnpkg.com/firebase-tools/-/firebase-tools-13.8.1.tgz#579b7bee6eee0b86bfa733c73749e356c1ac3670" - integrity sha512-DX6QhPh1A7ctNvrIP3zhu59YAu9AdzvUW7A9D2FkfjmS6XCmNct6tTGhMW0N3tcbPUvlnqZzzePFDy7+JUWsUQ== +firebase-tools@^13.8.0: + version "13.8.2" + resolved "https://registry.yarnpkg.com/firebase-tools/-/firebase-tools-13.8.2.tgz#daa46ab0015a9c6df4c2a0f4d9da101145883efc" + integrity sha512-63g2mUzRZeumla26lTr2jmenLQ3g7im75ExfoXjLx1TwB1KlMqyoTk58jcGPde0Vz4ZFTAxNLihsbWhziNhQHw== dependencies: "@google-cloud/cloud-sql-connector" "^1.2.3" "@google-cloud/pubsub" "^3.0.1" @@ -10199,6 +10199,13 @@ synckit@0.9.0, synckit@^0.9.0: "@pkgr/core" "^0.1.0" tslib "^2.6.2" +tailwind-merge@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.3.0.tgz#27d2134fd00a1f77eca22bcaafdd67055917d286" + integrity sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA== + dependencies: + "@babel/runtime" "^7.24.1" + tailwindcss@^3.4.1: version "3.4.3" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz"