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
+
+
+
+ );
+}
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;