diff --git a/src/components/LibraryPage/LibraryPage.test.tsx b/src/components/LibraryPage/LibraryPage.test.tsx
index f86b18d..e519fb7 100644
--- a/src/components/LibraryPage/LibraryPage.test.tsx
+++ b/src/components/LibraryPage/LibraryPage.test.tsx
@@ -1,46 +1,38 @@
-import { render, screen, fireEvent, act } from "@testing-library/react";
-import { describe, expect, it, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
import { LibraryPage } from "./LibraryPage";
-import { BooksProps } from "./components";
+import { useLibraryPage } from "./hooks";
+import { vi } from "vitest";
-const mockBooks = vi.fn();
-vi.mock("./components/Books/Books", () => ({
- Books: (props: BooksProps) => {
- mockBooks(props);
- return
Books
;
- },
+vi.mock("./hooks", () => ({
+ useLibraryPage: vi.fn(),
}));
-describe("LibraryPage", () => {
- it("renders correctly", () => {
- render();
-
- expect(screen.getByText("Books")).toBeInTheDocument();
+vi.mock("./components/Books", () => ({
+ Books: () => Books
,
+}));
- expect(mockBooks).toHaveBeenCalledWith(
- expect.objectContaining({
- search: "", // Initial debouncedSearch value
- limit: 50,
- })
- );
- });
+vi.mock("./components/Search", () => ({
+ Search: () => Search
,
+}));
- it("updates search state correctly", async () => {
- render();
+vi.mock("./components/UserAuthentication", () => ({
+ UserAuthentication: () => UserAuthentication
,
+}));
- // Simulate user input to the SearchInput component
- fireEvent.change(screen.getByLabelText("Search by title or author"), {
- target: { value: "New search value" },
+describe("LibraryPage", () => {
+ it("renders without crashing", () => {
+ (useLibraryPage as jest.Mock).mockReturnValue({
+ search: "",
+ setSearch: vi.fn(),
+ debouncedSearch: "",
+ error: null,
+ handleLogout: vi.fn(),
+ setError: vi.fn(),
});
- // Wait for the debounce delay
- await act(() => new Promise((resolve) => setTimeout(resolve, 500)));
-
- // Check that the Books component was called with the updated debouncedSearch value
- expect(mockBooks).toHaveBeenCalledWith(
- expect.objectContaining({
- search: "New search value",
- })
- );
+ render();
+ expect(screen.getByText("Books")).toBeInTheDocument();
+ expect(screen.getByText("Search")).toBeInTheDocument();
+ expect(screen.getByText("UserAuthentication")).toBeInTheDocument();
});
});
diff --git a/src/components/LibraryPage/LibraryPage.tsx b/src/components/LibraryPage/LibraryPage.tsx
index fff8493..a7bdb56 100644
--- a/src/components/LibraryPage/LibraryPage.tsx
+++ b/src/components/LibraryPage/LibraryPage.tsx
@@ -1,84 +1,16 @@
-import { ChangeEvent, Dispatch, SetStateAction } from "react";
import { useLibraryPage } from "./hooks";
-import {
- Alert,
- Avatar,
- Box,
- CircularProgress,
- Grid,
- Menu,
- MenuItem,
- Snackbar,
-} from "@mui/material";
-import { Root, StyledTextField } from "./LibraryPage.styles";
-import { Books } from "./components";
-import { LoginButton } from "./components/LoginButton/LoginButton";
-
-interface SearchInputProps {
- search: string;
- setSearch: Dispatch>;
-}
-
-const SearchInput = ({ search, setSearch }: SearchInputProps) => {
- const handleSearchChange = (event: ChangeEvent) => {
- setSearch(event.target.value);
- };
-
- return (
-
-
-
- );
-};
+import { Alert, Box, Grid, Snackbar } from "@mui/material";
+import { Root } from "./LibraryPage.styles";
+import { Books, Search, UserAuthentication } from "./components";
export const LibraryPage = () => {
- const {
- search,
- setSearch,
- debouncedSearch,
- error,
- user,
- anchorEl,
- handleMenuOpen,
- handleMenuClose,
- handleLogout,
- setError,
- userState,
- } = useLibraryPage();
-
- const userStates = {
- loading: ,
- authenticated:
- user && user.name ? (
-
-
{user?.name[0]}
-
-
- ) : null,
- unauthenticated: ,
- };
+ const { search, setSearch, debouncedSearch, error, setError } =
+ useLibraryPage();
return (
setError(null)}
@@ -88,15 +20,15 @@ export const LibraryPage = () => {
-
-
+
+
- {userStates[userState]}
+
-
+
-
+
);
diff --git a/src/components/LibraryPage/components/Books/Books.tsx b/src/components/LibraryPage/components/Books/Books.tsx
index 5e1cbdf..dbc88ef 100644
--- a/src/components/LibraryPage/components/Books/Books.tsx
+++ b/src/components/LibraryPage/components/Books/Books.tsx
@@ -70,14 +70,7 @@ export const Books = ({ search, limit }: BooksProps) => {
{loading ? (
-
+
) : books.length > 0 ? (
diff --git a/src/components/LibraryPage/components/Books/components/BookCard/BookCard.tsx b/src/components/LibraryPage/components/Books/components/BookCard/BookCard.tsx
index 53d3a72..fc7ae9f 100644
--- a/src/components/LibraryPage/components/Books/components/BookCard/BookCard.tsx
+++ b/src/components/LibraryPage/components/Books/components/BookCard/BookCard.tsx
@@ -20,7 +20,7 @@ interface BookCardProps {
}
export const BookCard: React.FC = ({ book }) => (
-
+
>;
+}
+
+export const Search = ({ search, setSearch }: SearchProps) => {
+ const handleSearchChange = (event: ChangeEvent) => {
+ setSearch(event.target.value);
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/src/components/LibraryPage/components/Search/SearchInput.styles.ts b/src/components/LibraryPage/components/Search/SearchInput.styles.ts
new file mode 100644
index 0000000..adcaa29
--- /dev/null
+++ b/src/components/LibraryPage/components/Search/SearchInput.styles.ts
@@ -0,0 +1,22 @@
+import { TextField } from "@mui/material";
+import { styled } from "@mui/material/styles";
+
+export const StyledTextField = styled(TextField)(({ theme }) => {
+ return `
+ .MuiInputBase-input {
+ background-color: ${
+ theme.palette?.mode === "dark"
+ ? "rgba(255, 255, 255, 0.15)"
+ : undefined
+ };
+ color: ${theme.palette?.mode === "dark" ? "#fff" : undefined};
+ &::placeholder {
+ color: ${
+ theme.palette?.mode === "dark"
+ ? "rgba(255, 255, 255, 0.5)"
+ : undefined
+ };
+ }
+ }
+ `;
+});
diff --git a/src/components/LibraryPage/components/Search/index.ts b/src/components/LibraryPage/components/Search/index.ts
new file mode 100644
index 0000000..f3cfe1b
--- /dev/null
+++ b/src/components/LibraryPage/components/Search/index.ts
@@ -0,0 +1 @@
+export * from "./Search";
diff --git a/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.test.tsx b/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.test.tsx
new file mode 100644
index 0000000..1d19ec8
--- /dev/null
+++ b/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.test.tsx
@@ -0,0 +1,40 @@
+import { render, fireEvent, waitFor, screen } from "@testing-library/react";
+import { UserAuthentication } from "./UserAuthentication";
+import { useAuth0 } from "@auth0/auth0-react";
+import { vi } from "vitest";
+
+vi.mock("@auth0/auth0-react", () => ({
+ useAuth0: vi.fn(),
+}));
+
+describe("UserAuthentication", () => {
+ it("should render loading state", () => {
+ (useAuth0 as jest.Mock).mockReturnValue({
+ isLoading: true,
+ });
+
+ render();
+ expect(screen.getByRole("progressbar")).toBeInTheDocument();
+ });
+
+ it("should render authenticated state", async () => {
+ (useAuth0 as jest.Mock).mockReturnValue({
+ isLoading: false,
+ user: { name: "Test User" },
+ });
+
+ render();
+ fireEvent.click(screen.getByText("T"));
+ await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument());
+ });
+
+ it("should render unauthenticated state", () => {
+ (useAuth0 as jest.Mock).mockReturnValue({
+ isLoading: false,
+ user: null,
+ });
+
+ render();
+ expect(screen.getByRole("button", { name: /log in/i })).toBeInTheDocument();
+ });
+});
diff --git a/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.tsx b/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.tsx
new file mode 100644
index 0000000..2e415ec
--- /dev/null
+++ b/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.tsx
@@ -0,0 +1,49 @@
+import { Avatar, CircularProgress, Menu, MenuItem } from "@mui/material";
+import { useAuth0 } from "@auth0/auth0-react";
+import { useState, useCallback } from "react";
+import { LoginButton } from "../LoginButton";
+
+export const UserAuthentication = () => {
+ const { user, isLoading, logout } = useAuth0();
+ const [anchorEl, setAnchorEl] = useState(null);
+
+ const handleMenuOpen = useCallback(
+ (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ },
+ []
+ );
+
+ const handleMenuClose = useCallback(() => {
+ setAnchorEl(null);
+ }, []);
+
+ const handleLogout = useCallback(
+ (event: React.MouseEvent) => {
+ event.preventDefault();
+ logout();
+ },
+ [logout]
+ );
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (user?.name) {
+ return (
+
+
{user.name[0]}
+
+
+ );
+ }
+
+ return ;
+};
diff --git a/src/components/LibraryPage/components/UserAuthentication/index.ts b/src/components/LibraryPage/components/UserAuthentication/index.ts
new file mode 100644
index 0000000..39b7edd
--- /dev/null
+++ b/src/components/LibraryPage/components/UserAuthentication/index.ts
@@ -0,0 +1 @@
+export * from "./UserAuthentication";
diff --git a/src/components/LibraryPage/components/index.ts b/src/components/LibraryPage/components/index.ts
index 355dfc2..0ddee35 100644
--- a/src/components/LibraryPage/components/index.ts
+++ b/src/components/LibraryPage/components/index.ts
@@ -1 +1,3 @@
export * from "./Books";
+export * from "./Search";
+export * from "./UserAuthentication";
diff --git a/src/components/LibraryPage/hooks/useLibraryPage.ts b/src/components/LibraryPage/hooks/useLibraryPage.ts
index 570a376..804fa8d 100644
--- a/src/components/LibraryPage/hooks/useLibraryPage.ts
+++ b/src/components/LibraryPage/hooks/useLibraryPage.ts
@@ -1,22 +1,12 @@
-import { useState, useCallback, useMemo } from "react";
+import { useState } from "react";
import { useDebounce } from "./useDebounce";
-import { User, useAuth0 } from "@auth0/auth0-react";
-
-type UserState = "loading" | "authenticated" | "unauthenticated";
interface LibraryPageHook {
search: string;
setSearch: React.Dispatch>;
debouncedSearch: string;
error: Error | null;
- user: User | undefined;
- anchorEl: null | HTMLElement;
- isLoading: boolean;
- handleMenuOpen: (event: React.MouseEvent) => void;
- handleMenuClose: () => void;
- handleLogout: (event: React.MouseEvent) => void;
setError: React.Dispatch>;
- userState: UserState;
}
export const useLibraryPage = (
@@ -25,53 +15,6 @@ export const useLibraryPage = (
const [error, setError] = useState(null);
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, debounceDelay);
- const { user, logout, isLoading } = useAuth0();
- const [anchorEl, setAnchorEl] = useState(null);
-
- const handleMenuOpen = useCallback(
- (event: React.MouseEvent) => {
- setAnchorEl(event.currentTarget);
- },
- []
- );
-
- const handleMenuClose = useCallback(() => {
- setAnchorEl(null);
- }, []);
-
- const handleLogout = useCallback(
- async (event: React.MouseEvent) => {
- event.preventDefault();
- try {
- await logout();
- } catch (error) {
- setError(new Error("Logout failed"));
- console.error("Logout failed");
- }
- },
- [logout]
- );
-
- const getUserState = (): UserState => {
- if (isLoading) return "loading";
- if (user && user.name) return "authenticated";
- return "unauthenticated";
- };
-
- const userState = useMemo(getUserState, [isLoading, user]);
- return {
- search,
- setSearch,
- debouncedSearch,
- isLoading,
- user,
- anchorEl,
- error,
- handleMenuOpen,
- handleMenuClose,
- handleLogout,
- setError,
- userState,
- };
+ return { search, setSearch, debouncedSearch, error, setError };
};