diff --git a/.gitignore b/.gitignore index ed25584..a65539c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ dist-ssr # Cache cache +# Firebase data +firebase/data + # Editor directories and files .idea .DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json index 0aca2cc..08d85ce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,6 +24,7 @@ "files.associations": { "*.css": "tailwindcss" }, + "explorer.compactFolders": false, // Language Specific Config "[nginx]": { diff --git a/cspell.json b/cspell.json index 3f7b64b..d672e03 100644 --- a/cspell.json +++ b/cspell.json @@ -1,4 +1,4 @@ { - "ignoreWords": ["tseslint", "tailwindcss", "testid", "svgr"], + "ignoreWords": ["tseslint", "tailwindcss", "testid", "svgr", "firestore"], "words": ["crwn"] } diff --git a/docker-compose.yml b/docker-compose.yml index cbf7952..038d13a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: - "4000:4001" # ui - "4600:4601" # logging - "9099:9100" # auth + - "8080:8081" # firestore volumes: - ./firebase:/srv/firebase:rw - ./cache:/root/.cache/:rw diff --git a/firebase/firestore.rules b/firebase/firestore.rules index 86411d2..c3c1733 100644 --- a/firebase/firestore.rules +++ b/firebase/firestore.rules @@ -1,3 +1,8 @@ rules_version = '2'; -service cloud.firestore { +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if true; + } + } } diff --git a/package-lock.json b/package-lock.json index ea88eab..7087838 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@eslint/js": "^9.9.0", "@testing-library/jest-dom": "^6.6.1", "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.1", @@ -2297,6 +2298,19 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", diff --git a/package.json b/package.json index f05d031..4a6c974 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@eslint/js": "^9.9.0", "@testing-library/jest-dom": "^6.6.1", "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.1", diff --git a/src/App.css b/src/App.css index efa9dda..5a1314d 100644 --- a/src/App.css +++ b/src/App.css @@ -5,3 +5,9 @@ body { a { color: black; } + +#root { + width: 80%; + max-width: 85rem; + margin: auto; +} diff --git a/src/components/NavigationBar.test.tsx b/src/components/NavigationBar.test.tsx index 1c848ad..b4e2c30 100644 --- a/src/components/NavigationBar.test.tsx +++ b/src/components/NavigationBar.test.tsx @@ -15,8 +15,8 @@ test("should have a link to the shop page", function () { expect(shopLinkElement).toHaveAttribute("href", "/shop"); }); -test("should have a link to the sign in page", function () { +test("should have a link to the authentication page", function () { render(, { wrapper: MemoryRouter }); const signInLinkElement = screen.getByText(/sign in/i); - expect(signInLinkElement).toHaveAttribute("href", "/sign-in"); + expect(signInLinkElement).toHaveAttribute("href", "/auth"); }); diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index c279c81..1e32db0 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -13,7 +13,7 @@ export default function NavigationBar() { shop
  • - sign in + sign in
  • diff --git a/src/components/UI/Button.test.tsx b/src/components/UI/Button.test.tsx new file mode 100644 index 0000000..094afd0 --- /dev/null +++ b/src/components/UI/Button.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from "@testing-library/react"; + +import Button from "./Button"; + +test("should render with default styles", () => { + render(); + const buttonElement = screen.getByText("Default Button"); + expect(buttonElement).toBeInTheDocument(); + expect(buttonElement).toHaveClass("bg-black text-white"); +}); + +test("should render with google-sign-in styles", () => { + render(); + const buttonElement = screen.getByText("Google Sign In"); + expect(buttonElement).toBeInTheDocument(); + expect(buttonElement).toHaveClass("bg-google text-white"); +}); + +test("should render with inverted styles", () => { + render(); + const buttonElement = screen.getByText("Inverted Button"); + expect(buttonElement).toBeInTheDocument(); + expect(buttonElement).toHaveClass( + "border-2 border-black bg-white text-black", + ); +}); + +test("should pass other props to the button element", () => { + render(); + const buttonElement = screen.getByText("Disabled Button"); + expect(buttonElement).toBeInTheDocument(); + expect(buttonElement).toBeDisabled(); +}); diff --git a/src/components/UI/Button.tsx b/src/components/UI/Button.tsx new file mode 100644 index 0000000..17724f0 --- /dev/null +++ b/src/components/UI/Button.tsx @@ -0,0 +1,33 @@ +import { ButtonHTMLAttributes, ReactNode } from "react"; +import tw from "../../utils/tw-identity"; + +interface ButtonProps extends ButtonHTMLAttributes { + children: ReactNode; + buttonType?: "inverted" | "google-sign-in"; +} + +export default function Button({ + children, + buttonType, + ...otherProps +}: ButtonProps) { + let classes = tw`h-12 min-w-40 px-8 font-bold uppercase hover:border-2` + " "; + + if (!buttonType) { + classes += tw`bg-black text-white hover:border-black hover:bg-white hover:text-black`; + } + + if (buttonType === "google-sign-in") { + classes += tw`bg-google text-white hover:border-google hover:bg-white hover:text-google`; + } + + if (buttonType === "inverted") { + classes += tw`border-2 border-black bg-white text-black hover:border-white hover:bg-black hover:text-white`; + } + + return ( + + ); +} diff --git a/src/components/UI/FormInput.test.tsx b/src/components/UI/FormInput.test.tsx new file mode 100644 index 0000000..e657c62 --- /dev/null +++ b/src/components/UI/FormInput.test.tsx @@ -0,0 +1,42 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import FormInput from "./FormInput"; + +test("should render with the correct label", () => { + render(); + const labelElement = screen.getByLabelText("Username"); + expect(labelElement).toBeInTheDocument(); +}); + +test("should apply shrink classes when input has value", () => { + render(); + const labelElement = screen.getByText("Username"); + expect(labelElement).toHaveClass("-top-5 text-xs text-black"); +}); + +test("should apply default classes when input is empty", () => { + render(); + const labelElement = screen.getByText("Username"); + expect(labelElement).toHaveClass("top-3 text-gray-600"); +}); + +test("should apply password classes when input type is password", () => { + render(); + const inputElement = screen.getByLabelText("Password"); + expect(inputElement).toHaveClass("tracking-wider"); +}); + +test("should focus input when label is clicked", async () => { + render(); + const labelElement = screen.getByText("Username"); + const inputElement = screen.getByLabelText("Username"); + await userEvent.click(labelElement); + expect(inputElement).toHaveFocus(); +}); + +test("should pass other props to the input element", () => { + render(); + const inputElement = screen.getByLabelText("Username"); + expect(inputElement).toBeDisabled(); +}); diff --git a/src/components/UI/FormInput.tsx b/src/components/UI/FormInput.tsx new file mode 100644 index 0000000..b6e57d0 --- /dev/null +++ b/src/components/UI/FormInput.tsx @@ -0,0 +1,29 @@ +import { InputHTMLAttributes } from "react"; + +import tw from "../../utils/tw-identity"; + +interface FormInputProps extends InputHTMLAttributes { + label: string; +} + +export default function FormInput({ label, ...otherProps }: FormInputProps) { + const inputId = label.toLowerCase().replace(" ", "-"); + + const shrinkClasses = otherProps.value?.toString().length + ? tw`-top-5 text-xs text-black` + : tw`top-3 text-gray-600`; + const passwordClasses = + otherProps.type === "password" ? "tracking-wider" : ""; + + const inputClasses = tw`peer my-6 block w-full border-b-2 border-b-gray-600 bg-slate-100 py-2.5 pl-1 pr-2.5 text-lg text-gray-600 focus:outline-none ${passwordClasses}`; + const labelClasses = tw`pointer-events-none absolute left-1 transition-all peer-focus:-top-5 peer-focus:text-xs peer-focus:text-black ${shrinkClasses}`; + + return ( +
    + + +
    + ); +} diff --git a/src/components/authentication/SignInForm.test.tsx b/src/components/authentication/SignInForm.test.tsx new file mode 100644 index 0000000..019a748 --- /dev/null +++ b/src/components/authentication/SignInForm.test.tsx @@ -0,0 +1,102 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import SignInForm from "./SignInForm"; + +const mockedMethods = vi.hoisted(function () { + return { + signInAuthUserFn: vi.fn(), + }; +}); + +vi.mock("../../utils/firebase", function () { + return { + signInAuthUserWithEmailAndPassword: mockedMethods.signInAuthUserFn, + }; +}); + +vi.spyOn(window, "alert").mockImplementation(() => {}); + +test("should render the correct titles", function () { + render(); + + expect(screen.getByRole("heading", { name: /sign in/i })).toBeInTheDocument(); + expect(screen.getByText(/already have an account/i)).toBeInTheDocument(); +}); + +test("should render the correct form fields", function () { + render(); + + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); +}); + +test("should render the correct buttons", function () { + render(); + + expect( + screen.getByRole("button", { name: /^sign in$/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /google sign in/i }), + ).toBeInTheDocument(); +}); + +test("should submit the form with the correct data", async function () { + render(); + + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/password/i); + const submitButton = screen.getByRole("button", { name: /^sign in$/i }); + + const user = userEvent.setup(); + + await user.type(emailInput, "john.doe@test.com"); + await user.type(passwordInput, "password"); + await user.click(submitButton); + + expect(mockedMethods.signInAuthUserFn).toHaveBeenCalledWith( + "john.doe@test.com", + "password", + ); +}); + +test("should show an alert if the password is incorrect", async function () { + mockedMethods.signInAuthUserFn.mockRejectedValue({ + code: "auth/wrong-password", + }); + + render(); + + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/password/i); + const submitButton = screen.getByRole("button", { name: /^sign in$/i }); + + const user = userEvent.setup(); + + await user.type(emailInput, "john.doe@test.com"); + await user.type(passwordInput, "password"); + await user.click(submitButton); + + expect(window.alert).toHaveBeenCalledWith("Wrong email or password"); +}); + +test("should show an alert if the user is not found", async function () { + mockedMethods.signInAuthUserFn.mockRejectedValue({ + code: "auth/user-not-found", + }); + + render(); + + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/password/i); + const submitButton = screen.getByRole("button", { name: /^sign in$/i }); + + const user = userEvent.setup(); + + await user.type(emailInput, "john.doe@test.com"); + await user.type(passwordInput, "password"); + await user.click(submitButton); + + expect(window.alert).toHaveBeenCalledWith("Wrong email or password"); +}); diff --git a/src/components/authentication/SignInForm.tsx b/src/components/authentication/SignInForm.tsx new file mode 100644 index 0000000..b551533 --- /dev/null +++ b/src/components/authentication/SignInForm.tsx @@ -0,0 +1,110 @@ +import { AuthError, getRedirectResult } from "firebase/auth"; +import { ChangeEvent, FormEvent, useEffect, useState } from "react"; + +import Button from "../UI/Button"; +import FormInput from "../UI/FormInput"; + +import { + auth, + createUserDocumentFromAuth, + signInAuthUserWithEmailAndPassword, + signInWithGooglePopup, + signInWithGoogleRedirect, +} from "../../utils/firebase"; + +const INITIAL_FORM_FIELDS = { + email: "", + password: "", +}; + +interface SignInFormProps { + useRedirect?: boolean; +} + +export default function SignInForm({ useRedirect = false }: SignInFormProps) { + const [formFields, setFormFields] = useState(INITIAL_FORM_FIELDS); + + useEffect(() => { + async function handleRedirectResult() { + const result = await getRedirectResult(auth); + if (result) await createUserDocumentFromAuth(result.user); + } + + if (useRedirect) handleRedirectResult(); + }, [useRedirect]); + + function handleChange(event: ChangeEvent) { + const { name, value } = event.target; + setFormFields(prev => ({ ...prev, [name]: value })); + } + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + + try { + await signInAuthUserWithEmailAndPassword( + formFields.email, + formFields.password, + ); + } catch (error) { + if ( + (error as AuthError).code === "auth/wrong-password" || + (error as AuthError).code === "auth/user-not-found" + ) { + alert("Wrong email or password"); + } + console.error("Error signing in", error); + } + } + + async function handleLoginWithGooglePopup() { + const response = await signInWithGooglePopup(); + await createUserDocumentFromAuth(response.user); + } + + async function handleRedirectWithGoogle() { + await signInWithGoogleRedirect(); + } + + return ( +
    + Already have an account? +

    + Sign in with your email and password +

    +
    + + +
    + + + {useRedirect && ( + + )} +
    + +
    + ); +} diff --git a/src/components/authentication/SignUpForm.test.tsx b/src/components/authentication/SignUpForm.test.tsx new file mode 100644 index 0000000..ec515fd --- /dev/null +++ b/src/components/authentication/SignUpForm.test.tsx @@ -0,0 +1,128 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import SignUpForm from "./SignUpForm"; + +const mockedMethods = vi.hoisted(function () { + return { + createAuthUserFn: vi.fn(), + createUserDocumentFn: vi.fn(), + }; +}); + +vi.mock("../../utils/firebase", function () { + return { + createAuthUserWithEmailAndPassword: mockedMethods.createAuthUserFn, + createUserDocumentFromAuth: mockedMethods.createUserDocumentFn, + }; +}); + +mockedMethods.createAuthUserFn.mockResolvedValue({ + user: { uid: "123" }, +}); + +vi.spyOn(window, "alert").mockImplementation(() => {}); + +beforeEach(function () { + vi.clearAllMocks(); +}); + +test("should render the correct titles", function () { + render(); + + expect(screen.getByRole("heading", { name: /sign up/i })).toBeInTheDocument(); + expect(screen.getByText(/don't have an account/i)).toBeInTheDocument(); +}); + +test("should render the correct form fields", function () { + render(); + + expect(screen.getByLabelText(/display name/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/^password/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/^confirm password/i)).toBeInTheDocument(); +}); + +test("should render the correct buttons", function () { + render(); + + expect( + screen.getByRole("button", { name: /^sign up$/i }), + ).toBeInTheDocument(); +}); + +test("should submit the form with the correct data", async function () { + render(); + + const displayNameInput = screen.getByLabelText(/display name/i); + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/^password/i); + const confirmPasswordInput = screen.getByLabelText(/^confirm password/i); + const submitButton = screen.getByRole("button", { name: /sign up/i }); + + const user = userEvent.setup(); + + await user.type(displayNameInput, "John Doe"); + await user.type(emailInput, "john.doe@test.com"); + await user.type(passwordInput, "password"); + await user.type(confirmPasswordInput, "password"); + await user.click(submitButton); + + expect(mockedMethods.createAuthUserFn).toHaveBeenCalledWith( + "john.doe@test.com", + "password", + ); + + expect(mockedMethods.createUserDocumentFn).toHaveBeenCalledWith( + { uid: "123" }, + { displayName: "John Doe" }, + ); +}); + +test("should show an alert if passwords do not match", async function () { + render(); + + const displayNameInput = screen.getByLabelText(/display name/i); + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/^password/i); + const confirmPasswordInput = screen.getByLabelText(/^confirm password/i); + const submitButton = screen.getByRole("button", { name: /sign up/i }); + + const user = userEvent.setup(); + + await user.type(displayNameInput, "John Doe"); + await user.type(emailInput, "john.doe@test.com"); + await user.type(passwordInput, "password"); + await user.type(confirmPasswordInput, "passphrase"); + await user.click(submitButton); + + expect(mockedMethods.createAuthUserFn).not.toHaveBeenCalled(); + expect(window.alert).toHaveBeenCalledWith("Passwords do not match"); +}); + +test("should show an alert if email is already in use", async function () { + mockedMethods.createAuthUserFn.mockRejectedValue({ + code: "auth/email-already-in-use", + }); + + render(); + + const displayNameInput = screen.getByLabelText(/display name/i); + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/^password/i); + const confirmPasswordInput = screen.getByLabelText(/^confirm password/i); + const submitButton = screen.getByRole("button", { name: /sign up/i }); + + const user = userEvent.setup(); + + await user.type(displayNameInput, "John Doe"); + await user.type(emailInput, "john.doe@test.com"); + await user.type(passwordInput, "password"); + await user.type(confirmPasswordInput, "password"); + await user.click(submitButton); + + expect(mockedMethods.createUserDocumentFn).not.toHaveBeenCalled(); + expect(window.alert).toHaveBeenCalledWith( + "Cannot create user, email already in use!", + ); +}); diff --git a/src/components/authentication/SignUpForm.tsx b/src/components/authentication/SignUpForm.tsx new file mode 100644 index 0000000..48b645a --- /dev/null +++ b/src/components/authentication/SignUpForm.tsx @@ -0,0 +1,96 @@ +import { AuthError } from "firebase/auth"; +import { ChangeEvent, FormEvent, useState } from "react"; + +import Button from "../UI/Button"; +import FormInput from "../UI/FormInput"; + +import { + createAuthUserWithEmailAndPassword, + createUserDocumentFromAuth, +} from "../../utils/firebase"; + +const INITIAL_FORM_FIELDS = { + displayName: "", + email: "", + password: "", + confirmPassword: "", +}; + +export default function SignUpForm() { + const [formFields, setFormFields] = useState(INITIAL_FORM_FIELDS); + + function handleChange(event: ChangeEvent) { + const { name, value } = event.target; + setFormFields(prev => ({ ...prev, [name]: value })); + } + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + + if (formFields.password !== formFields.confirmPassword) { + return alert("Passwords do not match"); + } + + try { + const userCredentials = await createAuthUserWithEmailAndPassword( + formFields.email, + formFields.password, + ); + await createUserDocumentFromAuth(userCredentials.user, { + displayName: formFields.displayName, + }); + + setFormFields(INITIAL_FORM_FIELDS); + } catch (error) { + if ((error as AuthError).code === "auth/email-already-in-use") { + return alert("Cannot create user, email already in use!"); + } + + console.error("Error signing up", error); + } + } + + return ( +
    + Don't have an account +

    + Sign up with your email and password +

    +
    + + + + + + +
    + ); +} diff --git a/src/pages/AuthenticationPage.test.tsx b/src/pages/AuthenticationPage.test.tsx new file mode 100644 index 0000000..c8c8ee5 --- /dev/null +++ b/src/pages/AuthenticationPage.test.tsx @@ -0,0 +1,13 @@ +import { render, screen } from "@testing-library/react"; + +import AuthenticationPage from "./AuthenticationPage"; + +test("should render the sign in form", function () { + render(); + expect(screen.getByRole("heading", { name: /sign in/i })).toBeInTheDocument(); +}); + +test("should render the sign up form", function () { + render(); + expect(screen.getByRole("heading", { name: /sign up/i })).toBeInTheDocument(); +}); diff --git a/src/pages/AuthenticationPage.tsx b/src/pages/AuthenticationPage.tsx new file mode 100644 index 0000000..7640a01 --- /dev/null +++ b/src/pages/AuthenticationPage.tsx @@ -0,0 +1,11 @@ +import SignInForm from "../components/authentication/SignInForm"; +import SignUpForm from "../components/authentication/SignUpForm"; + +export default function AuthenticationPage() { + return ( +
    + + +
    + ); +} diff --git a/src/pages/SignInPage.test.tsx b/src/pages/SignInPage.test.tsx deleted file mode 100644 index 31d9acc..0000000 --- a/src/pages/SignInPage.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { render, screen } from "@testing-library/react"; - -import SignInPage from "./SignInPage"; - -test("should render the page title", function () { - render(); - const pageTitleElement = screen.getByText(/sign in page/i); - expect(pageTitleElement).toBeInTheDocument(); -}); diff --git a/src/pages/SignInPage.tsx b/src/pages/SignInPage.tsx deleted file mode 100644 index fb43671..0000000 --- a/src/pages/SignInPage.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { signInWithGoogle } from "../utils/firebase"; - -export default function SignInPage() { - const handleLoginWithGoogle = async () => { - const response = await signInWithGoogle(); - console.log(response); - }; - - return ( -
    -

    Sign In Page

    - -
    - ); -} diff --git a/src/routes.tsx b/src/routes.tsx index 2852207..29fa36e 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,9 +1,9 @@ import { RouteObject } from "react-router-dom"; +import AuthenticationPage from "./pages/AuthenticationPage"; import HomePage from "./pages/HomePage"; import RootLayout from "./pages/layouts/RootLayout"; import ShopPage from "./pages/ShopPage"; -import SignInPage from "./pages/SignInPage"; import { getCategories } from "./api/category"; @@ -14,7 +14,7 @@ export const routes: RouteObject[] = [ children: [ { index: true, element: , loader: getCategories }, { path: "shop", element: }, - { path: "sign-in", element: }, + { path: "auth", element: }, ], }, ]; diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index 9fa9c2a..56d3734 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -1,25 +1,81 @@ import { connectAuthEmulator, + createUserWithEmailAndPassword, getAuth, GoogleAuthProvider, + signInWithEmailAndPassword, signInWithPopup, + signInWithRedirect, + User, } from "firebase/auth"; +import { + connectFirestoreEmulator, + doc, + getDoc, + getFirestore, + setDoc, +} from "firebase/firestore"; import firebaseApp from "../config/firebase"; const FIREBASE_AUTH_EMULATOR = import.meta.env.VITE_FIREBASE_AUTH_EMULATOR_HOST; -const provider = new GoogleAuthProvider(); +const googleAuthProvider = new GoogleAuthProvider(); -provider.setCustomParameters({ +googleAuthProvider.setCustomParameters({ prompt: "select_account", }); export const auth = getAuth(firebaseApp); +export const db = getFirestore(); if (FIREBASE_AUTH_EMULATOR) { console.warn(`Using Firebase Auth Emulator on ${FIREBASE_AUTH_EMULATOR}...`); connectAuthEmulator(auth, "http://" + FIREBASE_AUTH_EMULATOR); + connectFirestoreEmulator(db, "localhost", 8080); +} + +export const signInWithGooglePopup = () => + signInWithPopup(auth, googleAuthProvider); +export const signInWithGoogleRedirect = () => + signInWithRedirect(auth, googleAuthProvider); + +export async function createUserDocumentFromAuth( + userAuth: User, + additionalData?: object, +) { + const userDocRef = doc(db, "users", userAuth.uid); + const userSnap = await getDoc(userDocRef); + + if (!userSnap.exists()) { + const { displayName, email } = userAuth; + const createdAt = new Date(); + + try { + await setDoc(userDocRef, { + displayName, + email, + createdAt, + ...additionalData, + }); + } catch (error) { + console.error("Error creating user document", error); + } + } + + return userDocRef; } -export const signInWithGoogle = () => signInWithPopup(auth, provider); +export async function createAuthUserWithEmailAndPassword( + email: string, + password: string, +) { + return createUserWithEmailAndPassword(auth, email, password); +} + +export async function signInAuthUserWithEmailAndPassword( + email: string, + password: string, +) { + return signInWithEmailAndPassword(auth, email, password); +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 6642f66..41f1677 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -3,7 +3,10 @@ import type { Config } from "tailwindcss"; export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { - extend: {}, + extend: { + letterSpacing: { wider: "3em" }, + colors: { google: "#4285f4" }, + }, }, plugins: [], } satisfies Config;