-
-
-
+
+
+
+
{filters}
-
+
-
+
{facets}
-
-
-
+
+
+
);
}
\ No newline at end of file
diff --git a/client/src/components/Facets/styles.tsx b/client/src/components/Facets/styles.tsx
new file mode 100644
index 0000000..a690326
--- /dev/null
+++ b/client/src/components/Facets/styles.tsx
@@ -0,0 +1,60 @@
+import { styled } from '@mui/material/styles';
+// Styled components using basic HTML elements to reduce bundle size
+export const FacetBox = styled('div')(() => ({
+ height: '100%',
+ boxShadow: 'none',
+ backgroundColor: 'transparent',
+ borderRadius: 0,
+}));
+
+export const FilterList = styled('ul')(() => ({
+ display: 'flex',
+ flexWrap: 'wrap',
+ padding: '8px 0',
+ margin: 0,
+ listStyle: 'none',
+}));
+
+// Custom lightweight chip component instead of MUI Chip
+export const StyledChip = styled('div')(() => ({
+ display: 'flex',
+ alignItems: 'center',
+ height: '32px',
+ margin: '4px',
+ padding: '0 12px',
+ fontSize: '0.8125rem',
+ backgroundColor: '#e0e0e0',
+ borderRadius: '16px',
+ cursor: 'default',
+ '&:hover': {
+ backgroundColor: '#bdbdbd',
+ },
+}));
+
+export const ChipLabel = styled('span')(() => ({
+ padding: '0 8px 0 0',
+}));
+
+export const ChipDeleteButton = styled('button')(() => ({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '16px',
+ height: '16px',
+ padding: 0,
+ fontSize: '14px',
+ lineHeight: 1,
+ color: '#666',
+ backgroundColor: 'transparent',
+ border: 'none',
+ borderRadius: '50%',
+ cursor: 'pointer',
+ '&:hover': {
+ backgroundColor: 'rgba(0, 0, 0, 0.1)',
+ },
+}));
+
+export const FacetList = styled('nav')(() => ({
+ marginTop: '32px',
+ padding: 0,
+}));
\ No newline at end of file
diff --git a/client/src/components/Pager.tsx b/client/src/components/Pager.tsx
new file mode 100644
index 0000000..9f198e0
--- /dev/null
+++ b/client/src/components/Pager.tsx
@@ -0,0 +1,89 @@
+import Pagination from '@mui/material/Pagination';
+import Box from '@mui/material/Box';
+import { styled } from '@mui/material/styles';
+
+// Use Material UI's styled API for better style isolation
+const StyledPagination = styled(Pagination)(() => ({
+ margin: '1em auto',
+ '& .MuiPaginationItem-root': {
+ color: '#0078d4',
+ },
+ '& .MuiPaginationItem-page.Mui-selected': {
+ backgroundColor: '#0078d4',
+ color: 'white',
+ '&:hover': {
+ backgroundColor: '#106ebe',
+ }
+ }
+}));
+
+// Constants
+const PAGE_WINDOW = 2; // Pages to show before and after the current page
+
+/**
+ * Pagination component for search results - fully controlled by parent
+ * @param {Object} props
+ * @param {number} props.currentPage - Current active page number (1-based)
+ * @param {number} props.resultCount - Total number of results across all pages
+ * @param {number} props.resultsPerPage - Number of results displayed per page
+ * @param {function} props.onPageChange - Callback function when page changes
+ */
+export default function Pager(props) {
+ // Destructure props for cleaner code and proper dependency tracking
+ const { currentPage, resultCount, resultsPerPage, onPageChange } = props;
+
+ // Ensure currentPage is always an integer
+ const page = parseInt(currentPage) || 1;
+ const totalPages = Math.max(1, Math.ceil(resultCount / resultsPerPage));
+
+ // Handler for changing the current page
+ function handlePageChange(pageNumber) {
+ // Convert to integer and clamp within valid range
+ const newPage = Math.max(1, Math.min(totalPages, parseInt(pageNumber) || 1));
+
+ // Only update if actually changing page
+ if (newPage !== page) {
+ onPageChange(newPage);
+ }
+ }
+
+ // With Material UI's Pagination component, we don't need the custom logic for
+ // next/previous page navigation or calculating page windows, as these are
+ // handled internally by the Pagination component
+
+ // With Material UI's Pagination component, we don't need to manually render page links
+ // or previous/next buttons, as they're handled by the component itself
+
+ // We also don't need the page window calculation for rendering since
+ // Material UI's Pagination handles this with siblingCount and boundaryCount props
+
+ // Handle case with no results
+ if (totalPages <= 0) {
+ return null; // No pagination needed when there are no results
+ }
+
+ return (
+
+ handlePageChange(newPage)}
+ showFirstButton
+ showLastButton
+ siblingCount={PAGE_WINDOW}
+ boundaryCount={1}
+ />
+
+ );
+}
diff --git a/client/src/components/Pager/Pager.css b/client/src/components/Pager/Pager.css
deleted file mode 100644
index de4d498..0000000
--- a/client/src/components/Pager/Pager.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.item {
- margin: 1em auto;
-}
-
-.pager {
- margin: auto;
- max-width: fit-content;
-}
\ No newline at end of file
diff --git a/client/src/components/Pager/Pager.jsx b/client/src/components/Pager/Pager.jsx
deleted file mode 100644
index 5820b81..0000000
--- a/client/src/components/Pager/Pager.jsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import React, { useMemo } from 'react';
-import PropTypes from 'prop-types';
-
-import './Pager.css';
-
-// Constants
-const PAGE_WINDOW = 2; // Pages to show before and after the current page
-
-/**
- * Pagination component for search results - fully controlled by parent
- * @param {Object} props
- * @param {number} props.currentPage - Current active page number (1-based)
- * @param {number} props.resultCount - Total number of results across all pages
- * @param {number} props.resultsPerPage - Number of results displayed per page
- * @param {function} props.onPageChange - Callback function when page changes
- */
-export default function Pager(props) {
- // Destructure props for cleaner code and proper dependency tracking
- const { currentPage, resultCount, resultsPerPage, onPageChange } = props;
-
- // Ensure currentPage is always an integer
- const page = parseInt(currentPage) || 1;
- const totalPages = Math.max(1, Math.ceil(resultCount / resultsPerPage));
-
- // Handler for changing the current page
- function handlePageChange(pageNumber) {
- // Convert to integer and clamp within valid range
- const newPage = Math.max(1, Math.min(totalPages, parseInt(pageNumber) || 1));
-
- // Only update if actually changing page
- if (newPage !== page) {
- onPageChange(newPage);
- }
- }
-
- // Handler for next page button click
- function handleNextPage() {
- if (page < totalPages) {
- handlePageChange(page + 1);
- }
- }
-
- // Handler for previous page button click
- function handlePreviousPage() {
- if (page > 1) {
- handlePageChange(page - 1);
- }
- }
-
- // Calculate page range and memoize to avoid recalculation on every render
- const { minPage, maxPage } = useMemo(() => {
- let minPage = Math.max(1, page - PAGE_WINDOW);
- let maxPage = Math.min(totalPages, page + PAGE_WINDOW);
-
- // Adjust range if we're near the start or end
- // This ensures we always show 5 pages if available
- if (maxPage - minPage < PAGE_WINDOW * 2) {
- if (page < totalPages / 2) {
- // Near start, expand end
- maxPage = Math.min(totalPages, minPage + PAGE_WINDOW * 2);
- } else {
- // Near end, expand start
- minPage = Math.max(1, maxPage - PAGE_WINDOW * 2);
- }
- }
-
- return { minPage, maxPage };
- }, [page, totalPages]);
-
- // Generate page links array
- function renderPageLinks() {
- const links = [];
-
- for (let i = minPage; i <= maxPage; i++) {
- if (i === page) {
- links.push(
-
-
- {i}
-
-
- );
- } else {
- links.push(
-
-
-
- );
- }
- }
- return links;
- }
-
- // Create previous button component
- function renderPreviousButton() {
- const isFirstPage = page === 1;
- return (
-
- {isFirstPage ? (
- Previous
- ) : (
-
- )}
-
- );
- }
-
- // Create next button component
- function renderNextButton() {
- const isLastPage = page === totalPages;
- return (
-
- {isLastPage ? (
- Next
- ) : (
-
- )}
-
- );
- }
-
- // Handle case with no results
- if (totalPages <= 0) {
- return null; // No pagination needed when there are no results
- }
-
- return (
-
- );
-}
-
-// PropTypes for better documentation and runtime type checking
-Pager.propTypes = {
- currentPage: PropTypes.number,
- resultCount: PropTypes.number.isRequired,
- resultsPerPage: PropTypes.number.isRequired,
- onPageChange: PropTypes.func.isRequired
-};
-
-// Default props
-Pager.defaultProps = {
- currentPage: 1
-};
diff --git a/client/src/components/Results/Result/Result.css b/client/src/components/Results/Result/Result.css
deleted file mode 100644
index 91aed91..0000000
--- a/client/src/components/Results/Result/Result.css
+++ /dev/null
@@ -1,54 +0,0 @@
-.result {
- width: 300px;
- padding: 8px;
- text-align: center;
- border: 1px solid #eee;
- box-shadow: 0 2px 3px #ccc;
- margin: 10px;
- margin-bottom: 3px;
- padding-bottom: 3px;
- box-sizing: border-box;
- cursor: pointer;
-}
-.card-img-top {
- width: 100%; /* Ensure the image takes up the full width of the container */
- height: 150px; /* Set a fixed height for uniformity */
- object-fit: contain; /* Ensure the image scales and crops to fit the box */
- display: block;
- margin: 0 auto;
- background-color: #f0f0f0; /* Optional: Add a background color for empty space */
-}
-.result:hover,
-.result:active {
- background-color: #C0DDF5;
-}
-
-.title-style {
- font-size: 0.9em;
- vertical-align: middle;
- white-space: normal; /* Allow wrapping to multiple lines */
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box; /* Use flexbox for multi-line ellipsis */
- -webkit-line-clamp: 2; /* Limit to 2 lines */
- -webkit-box-orient: vertical;
- line-height: 1.4em; /* Adjust line height for better spacing */
- max-height: 2.8em; /* Ensure height fits 2 lines */
- margin: 0; /* Remove any extra margin */
- color: #0078d7;
-}
-
-.card-body {
- padding: 0 !important;
- margin: 0;
- text-align: center;
- height: 3.2em; /* Fixed height to support 2 rows of text */
- display: flex; /* Use flexbox for alignment */
- justify-content: center; /* Center the text horizontally */
- align-items: center; /* Center the text vertically */
- box-shadow: none !important;
- border: none !important;
-}
-.result a {
- text-decoration: none; /* Remove underline from links */
-}
diff --git a/client/src/components/Results/Result/Result.jsx b/client/src/components/Results/Result/Result.jsx
deleted file mode 100644
index a46957c..0000000
--- a/client/src/components/Results/Result/Result.jsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-
-import './Result.css';
-
-export default function Result(props) {
- const title = props.document.original_title || '
';
-
- return(
-
- );
-}
diff --git a/client/src/components/Results/Result/Result.tsx b/client/src/components/Results/Result/Result.tsx
new file mode 100644
index 0000000..778beb3
--- /dev/null
+++ b/client/src/components/Results/Result/Result.tsx
@@ -0,0 +1,49 @@
+import Box from '@mui/material/Box';
+import { ResultProps } from '../../../types/props';
+import {
+ ResultCard,
+ ResultImage,
+ TitleText
+} from './styled.jsx';
+
+export default function Result(props: ResultProps) {
+ const title = props.document.original_title || '';
+
+
+ console.log(props.document);
+
+ return (
+
+
+
+ {/* Using div with inline styles instead of Box component to reduce bundle size */}
+
+ {/* Image section - fixed height */}
+
+
+
+
+ {/* Text section - centered in remaining space */}
+
+
+ {title}
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/Results/Result/styled.tsx b/client/src/components/Results/Result/styled.tsx
new file mode 100644
index 0000000..c5ec16d
--- /dev/null
+++ b/client/src/components/Results/Result/styled.tsx
@@ -0,0 +1,47 @@
+import { styled } from '@mui/material/styles';
+// Styled div instead of Card to reduce bundle size
+export const ResultCard = styled('div')(() => ({
+ width: '200px',
+ padding: '8px',
+ textAlign: 'center',
+ border: '1px solid #eee',
+ boxShadow: '0 2px 3px #ccc',
+ margin: '10px',
+ marginBottom: '3px',
+ paddingBottom: '8px',
+ cursor: 'pointer',
+ transition: 'background-color 0.3s',
+ height: '220px', // Fixed height for consistent card sizing
+ display: 'flex',
+ flexDirection: 'column',
+ overflow: 'hidden', // Prevent content overflow
+ '&:hover': {
+ backgroundColor: '#C0DDF5',
+ },
+}));
+
+// Styled img instead of CardMedia to reduce bundle size
+export const ResultImage = styled('img')(() => ({
+ width: '100%',
+ height: '100%',
+ objectFit: 'contain',
+ display: 'block',
+ margin: '0 auto',
+ backgroundColor: '#ffffff',
+}));
+
+// Styled div instead of Typography to reduce bundle size
+export const TitleText = styled('div')(() => ({
+ fontSize: '0.9em',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ display: '-webkit-box',
+ WebkitLineClamp: 2,
+ WebkitBoxOrient: 'vertical',
+ lineHeight: '1.4em',
+ minHeight: '2.8em', // Force minimum height for 2 lines
+ color: '#0078d7',
+ padding: '0 8px',
+ textAlign: 'center',
+ width: '100%'
+}));
\ No newline at end of file
diff --git a/client/src/components/Results/Results.css b/client/src/components/Results/Results.css
deleted file mode 100644
index 623ed43..0000000
--- a/client/src/components/Results/Results.css
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Detail Styles */
-.results-info {
- margin: 1em;
-}
-
-.results {
- display: flex;
- flex-flow: row wrap;
- justify-content: center;
- width: 100%;
- margin: auto;
- margin-left: 0em;
- margin-right: 0em;
-}
-
-.results .result {
- width: 10rem;
- max-height: 18rem;
-}
\ No newline at end of file
diff --git a/client/src/components/Results/Results.jsx b/client/src/components/Results/Results.jsx
deleted file mode 100644
index dfb5b6b..0000000
--- a/client/src/components/Results/Results.jsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import Result from './Result/Result';
-
-import "./Results.css";
-
-export default function Results(props) {
-
- let results = props.documents.map((result, index) => {
- return ;
- });
-
- let beginDocNumber = Math.min(props.skip + 1, props.count);
- let endDocNumber = Math.min(props.skip + props.top, props.count);
-
- return (
-
-
Showing {beginDocNumber}-{endDocNumber} of {props.count.toLocaleString()} results for {props.query}
-
- {results}
-
-
- );
-};
diff --git a/client/src/components/Results/Results.tsx b/client/src/components/Results/Results.tsx
new file mode 100644
index 0000000..dee7c9e
--- /dev/null
+++ b/client/src/components/Results/Results.tsx
@@ -0,0 +1,50 @@
+import Box from '@mui/material/Box';
+import Result from './Result/Result';
+import { ResultsProps } from '../../types/props';
+import {
+ ResultsContainer,
+ ResultsInfo
+} from './styled';
+
+export default function Results(props: ResultsProps) {
+
+ let results = props.searchResultDocuments.map((result, index) => {
+
+ let book = result?.document;
+
+ return ;
+ });
+
+ console.log(results[0]);
+
+ // Provide default values for pagination properties
+ const skip = props.skip ?? 0; // Default to 0 if skip is not provided
+ const count = props.count ?? 0; // Default to 0 if count is not provided
+ const top = props.top ?? 8; // Default to 8 if top is not provided
+
+ // When there are results, show 1-based counting for beginDocNumber
+ // When no results, beginDocNumber should be 0
+ let beginDocNumber = count > 0 ? skip + 1 : 0;
+
+ // For endDocNumber, take the smaller of (skip + top) or count
+ // This ensures we don't show ranges beyond the actual number of results
+ let endDocNumber = count > 0 ? Math.min(skip + top, count) : 0;
+
+ return (
+
+
+ {count > 0 ? (
+ <>Showing {beginDocNumber}-{endDocNumber} of {count.toLocaleString()} results for {props.query ?? ""}>
+ ) : (
+ <>No results found for {props.query ?? ""}>
+ )}
+
+
+ {results}
+
+
+ );
+};
diff --git a/client/src/components/Results/styled.tsx b/client/src/components/Results/styled.tsx
new file mode 100644
index 0000000..77e67c2
--- /dev/null
+++ b/client/src/components/Results/styled.tsx
@@ -0,0 +1,21 @@
+import { styled } from '@mui/material/styles';
+import Grid from '@mui/material/Grid';
+import Typography from '@mui/material/Typography';
+
+// Styled components for MUI isolation
+export const ResultsContainer = styled(Grid)(() => ({
+ display: 'flex',
+ flexFlow: 'row wrap',
+ justifyContent: 'center',
+ width: '100%',
+ margin: 'auto',
+ '& > *': {
+ height: 'auto', // Allow natural height
+ alignSelf: 'stretch' // Make each grid item stretch to fill its cell
+ }
+}));
+
+export const ResultsInfo = styled(Typography)(() => ({
+ margin: '1em',
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
+}));
\ No newline at end of file
diff --git a/client/src/components/SearchBar/SearchBar.css b/client/src/components/SearchBar/SearchBar.css
deleted file mode 100644
index 32fae97..0000000
--- a/client/src/components/SearchBar/SearchBar.css
+++ /dev/null
@@ -1,46 +0,0 @@
-.search-button {
- font-size: 14px;
- margin-left: 10px;
- border-color: 'green'
-}
-
-.suggestions {
- position: relative;
- display: inline-block;
- width: inherit;
- z-index:99
-}
-
-.search-bar {
- flex-wrap: nowrap;
- display: flex;
- justify-content: center;
- align-items: center;
- margin: 0 auto;
-}
-.search-bar-narrow {
-
-
-}
-
-.search-bar-wide {
- width: 40%;
- margin-left: 5px;
-}
-
-/* Moved styles from SearchBar.jsx */
-.autocomplete {
- width: 75%;
-}
-
-.autocomplete .MuiAutocomplete-endAdornment {
- display: none;
-}
-
-/* Added styles for the Box component */
-.search-bar-box {
- display: flex;
- align-items: center;
- flex: 1;
-}
-
diff --git a/client/src/components/SearchBar/SearchBar.jsx b/client/src/components/SearchBar/SearchBar.jsx
deleted file mode 100644
index c3900cd..0000000
--- a/client/src/components/SearchBar/SearchBar.jsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { TextField, Autocomplete, Button, Box } from '@mui/material';
-import fetchInstance from '../../url-fetch';
-import './SearchBar.css';
-
-export default function SearchBar({ postSearchHandler, query, width }) {
- const [q, setQ] = useState(() => query || '');
- const [suggestions, setSuggestions] = useState([]);
-
- const search = (value) => {
- postSearchHandler(value);
- };
-
- useEffect(() => {
- if (q) {
-
- const body = { q, top: 5, suggester: 'sg' };
-
- fetchInstance('/api/suggest', { body, method: 'POST' })
- .then(response => {
- setSuggestions(response.suggestions.map(s => s.text));
- })
- .catch(error => {
- console.log(error);
- setSuggestions([]);
- });
- }
- }, [q]);
-
-
- const onInputChangeHandler = (event, value) => {
- setQ(value);
- };
-
-
- const onChangeHandler = (event, value) => {
-
- setQ(value);
- search(value);
- };
-
- const onEnterButton = (event) => {
- // if enter key is pressed
- if (event.key === 'Enter') {
- search(q);
- }
- };
-
- return (
-
-
- (
- setSuggestions([])}
- onClick={() => setSuggestions([])}
- />
- )}
- />
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx
new file mode 100644
index 0000000..c4fb80e
--- /dev/null
+++ b/client/src/components/SearchBar/SearchBar.tsx
@@ -0,0 +1,96 @@
+import { useState, useEffect, useRef, KeyboardEvent, ChangeEvent } from 'react';
+import Box from '@mui/material/Box';
+import fetchInstance from '../../url-fetch';
+import { SearchBarProps } from '../../types/props';
+import { SuggestRequest, SuggestResponse } from '../../types/api';
+import {
+ SearchContainer,
+ SearchBox,
+ SearchInput,
+ SearchButton,
+ SuggestionList,
+ SuggestionItem
+} from './styles';
+
+export default function SearchBar({ postSearchHandler, query, width }: SearchBarProps) {
+ const [q, setQ] = useState(() => query || '');
+ const [suggestions, setSuggestions] = useState([]);
+
+ const search = (value: string): void => {
+ postSearchHandler(value);
+ };
+
+ useEffect(() => {
+ if (q) {
+ const body: SuggestRequest = { q, top: 5, suggester: 'sg' };
+
+ fetchInstance('/api/suggest', { body, method: 'POST' })
+ .then((response: SuggestResponse) => {
+ setSuggestions(response.suggestions.map((s) => s.text));
+ })
+ .catch((error: Error) => {
+ console.log(error);
+ setSuggestions([]);
+ });
+ } else {
+ setSuggestions([]);
+ }
+ }, [q]);
+
+ // Handle enter key in the search field
+ const handleKeyPress = (event: KeyboardEvent): void => {
+ if (event.key === 'Enter') {
+ search(q);
+ }
+ };
+
+ const [showSuggestions, setShowSuggestions] = useState(false);
+ const inputRef = useRef(null);
+
+ // Handle selecting suggestion
+ const handleSuggestionClick = (suggestion: string): void => {
+ setQ(suggestion);
+ search(suggestion);
+ setShowSuggestions(false);
+ };
+
+ return (
+
+
+
+
+ ) => setQ(e.target.value)}
+ onFocus={() => setShowSuggestions(true)}
+ onBlur={() => {
+ // Delay to allow click on suggestion
+ setTimeout(() => setShowSuggestions(false), 200);
+ }}
+ onKeyPress={handleKeyPress}
+ id="search-box"
+ placeholder="What are you looking for?"
+ style={{ width: '100%' }}
+ />
+ {showSuggestions && suggestions.length > 0 && (
+
+ {suggestions.map((suggestion: string, index: number) => (
+ handleSuggestionClick(suggestion)}
+ >
+ {suggestion}
+
+ ))}
+
+ )}
+
+ search(q)}>
+ Search
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/SearchBar/styles.tsx b/client/src/components/SearchBar/styles.tsx
new file mode 100644
index 0000000..d27ee66
--- /dev/null
+++ b/client/src/components/SearchBar/styles.tsx
@@ -0,0 +1,75 @@
+import { styled } from '@mui/material/styles';
+// Using styled HTML elements instead of MUI components to reduce bundle size
+export const SearchContainer = styled('div')(() => ({
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ margin: '0 auto',
+ width: '100%',
+}));
+
+export const SearchBox = styled('div')(() => ({
+ display: 'flex',
+ alignItems: 'center',
+ width: '100%',
+ // Make sure the input field takes up most of the space
+ '& > div': {
+ flexGrow: 1,
+ width: 'calc(100% - 100px)', // Accounting for button width + margin
+ }
+}));
+
+// Custom lightweight autocomplete implementation
+export const SearchInput = styled('input')(() => ({
+ width: '100%',
+ padding: '10px 14px',
+ fontSize: '16px',
+ borderRadius: '4px',
+ border: '1px solid #ccc',
+ outline: 'none',
+ height: '40px',
+ boxSizing: 'border-box',
+ '&:focus': {
+ borderColor: '#1976d2',
+ boxShadow: '0 0 0 2px rgba(25, 118, 210, 0.2)'
+ }
+}));
+
+export const SuggestionList = styled('ul')(() => ({
+ position: 'absolute',
+ zIndex: 1000,
+ background: 'white',
+ width: '100%',
+ maxHeight: '200px',
+ overflowY: 'auto',
+ borderRadius: '4px',
+ boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
+ margin: 0,
+ padding: 0,
+ listStyle: 'none',
+}));
+
+export const SuggestionItem = styled('li')(() => ({
+ padding: '8px 14px',
+ cursor: 'pointer',
+ '&:hover': {
+ backgroundColor: '#f5f5f5'
+ }
+}));
+
+export const SearchButton = styled('button')(() => ({
+ marginLeft: '8px',
+ height: '40px',
+ minWidth: '80px', // Fixed minimum width for the button
+ padding: '0 16px',
+ backgroundColor: '#1976d2',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ fontWeight: 500,
+ cursor: 'pointer',
+ flexShrink: 0, // Prevent button from shrinking
+ '&:hover': {
+ backgroundColor: '#1565c0'
+ }
+}));
\ No newline at end of file
diff --git a/client/src/contexts/AuthContext.jsx b/client/src/contexts/AuthContext.jsx
deleted file mode 100644
index dcb883b..0000000
--- a/client/src/contexts/AuthContext.jsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { createContext, useContext } from 'react';
-
-// Create new auth context
-export const AuthContext = createContext();
-
-// React hook to use auth context
-export function useAuth() {
- return useContext(AuthContext);
-}
diff --git a/client/src/debug.ts b/client/src/debug.ts
new file mode 100644
index 0000000..581044f
--- /dev/null
+++ b/client/src/debug.ts
@@ -0,0 +1,52 @@
+/**
+ * Debug utility for styling components
+ * Used to visualize component boundaries during development
+ */
+
+// Set this to true to enable debug mode
+export const DEBUG_MODE = false;
+
+// Debug styles to be imported and used in your app
+export const debugStyles = DEBUG_MODE
+ ? {
+ // Debug styles for all elements
+ '*': {
+ outline: '1px solid red',
+ },
+
+ // Debug styles specifically for MUI components
+ '.MuiBox-root': {
+ outline: '1px solid blue !important',
+ },
+ '.MuiGrid-root': {
+ outline: '1px solid green !important',
+ },
+ '.MuiContainer-root': {
+ outline: '1px solid purple !important',
+ },
+
+ // Debug styles for basic elements
+ 'div': {
+ outline: '1px dashed red',
+ },
+ 'section': {
+ outline: '1px dashed orange',
+ },
+ 'header, footer': {
+ outline: '2px solid magenta',
+ },
+
+ // Optional - Add component name label to top-left corner of selected elements
+ '.MuiBox-root:before, .MuiGrid-root:before, .MuiContainer-root:before': {
+ content: 'attr(class)',
+ position: 'absolute',
+ top: '0',
+ left: '0',
+ fontSize: '10px',
+ backgroundColor: 'rgba(255, 255, 255, 0.8)',
+ padding: '2px',
+ color: 'black',
+ zIndex: 1000,
+ },
+ }
+ : {}; // Empty object when debug mode is off
diff --git a/client/src/index.css b/client/src/index.css
deleted file mode 100644
index a78f33f..0000000
--- a/client/src/index.css
+++ /dev/null
@@ -1,69 +0,0 @@
-:root {
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
- padding: 0;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
diff --git a/client/src/main.jsx b/client/src/main.jsx
deleted file mode 100644
index fb92537..0000000
--- a/client/src/main.jsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './App/App.css'
-import App from './App/App.jsx'
-
-
-console.log('client2');
-
-createRoot(document.getElementById('root')).render(
-
-
- ,
-)
diff --git a/client/src/main.tsx b/client/src/main.tsx
new file mode 100644
index 0000000..55a2a2b
--- /dev/null
+++ b/client/src/main.tsx
@@ -0,0 +1,22 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { ThemeProvider } from '@mui/material/styles'
+import CssBaseline from '@mui/material/CssBaseline'
+import GlobalStyles from '@mui/material/GlobalStyles'
+import theme, { globalStyles } from './theme'
+import App from './App/App'
+
+const rootElement = document.getElementById('root');
+if (!rootElement) {
+ throw new Error("Root element with id 'root' not found.");
+}
+
+createRoot(rootElement).render(
+
+
+
+
+
+
+
+)
\ No newline at end of file
diff --git a/client/src/pages/Details/Details.css b/client/src/pages/Details/Details.css
deleted file mode 100644
index cb5ac60..0000000
--- a/client/src/pages/Details/Details.css
+++ /dev/null
@@ -1,88 +0,0 @@
-.main--details {
- padding-top: 2em;
-}
-.tab-panel{
- width: 100%;
-}
-.tab-panel-value{
- padding: 3;
-}
-
-.image {
- width: 10em;
- height: auto;
-}
-.card-body {
- height: inherit; /* Allow the card body to grow with content */
- position: relative; /* Ensure the card body is positioned relative to its container */
- background-color: #fff; /* Add a background color to the card */
- border: 1px solid #ddd; /* Add a border to define the card */
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Add a subtle shadow for depth */
- padding: 1em; /* Add padding for spacing */
- margin: 1em 0; /* Add margin to separate the card from other elements */
- display: flex; /* Use flexbox for layout */
- flex-direction: column; /* Stack elements vertically */
- align-items: center; /* Center elements horizontally */
- justify-content: flex-start; /* Align elements to the top */
- width: 100%; /* Ensure the card takes up the full width of the container */
- box-sizing: border-box; /* Include padding and border in the width/height */
-}
-
-.image {
- width: 10em; /* Set a fixed width for the image */
- height: auto; /* Maintain the aspect ratio */
- margin-bottom: 1em; /* Add spacing below the image */
-}
-
-.card-title {
- font-size: 1.2em;
- font-weight: bold;
- margin-bottom: 0.5em;
- text-align: center;
-}
-
-.card-text {
- font-size: 1em;
- margin-bottom: 0.5em;
- text-align: center;
-}
-
-.tab-panel {
- position: relative; /* Ensure proper positioning */
- width: 100%; /* Take up full width */
- overflow: visible; /* Allow content to expand */
- background-color: #fff; /* Optional: Add a background color */
- padding: 1em; /* Add padding for spacing */
- box-sizing: border-box; /* Include padding and border in width/height */
- min-height: auto; /* Allow the height to grow dynamically */
-}
-
-/* Styles for the header box */
-.box-header {
- border-bottom: 1px solid var(--divider-color);
- padding: 1em;
- background-color: #f5f5f5;
-}
-
-/* Styles for the content box */
-.box-content {
- padding: 1.5em;
- background-color: #ffffff;
- border: 1px solid #ddd;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-
-.details-box-parent {
- width: 100%;
- padding-top: 25px;
- padding-left: 150px;
- padding-right: 150px;
-}
-.details-tab-box-header {
- border-bottom: 1px solid;
- border-color: var(--divider-color);
-}
-.details-custom-tab-panel-json-div {
- text-align: left;
-}
-
diff --git a/client/src/pages/Details/Details.jsx b/client/src/pages/Details/Details.jsx
deleted file mode 100644
index 767ce81..0000000
--- a/client/src/pages/Details/Details.jsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useParams } from 'react-router-dom';
-import Rating from '@mui/material/Rating';
-import CircularProgress from '@mui/material/CircularProgress';
-import Tabs from '@mui/material/Tabs';
-import Tab from '@mui/material/Tab';
-import Box from '@mui/material/Box';
-
-import fetchInstance from '../../url-fetch';
-
-import "./Details.css";
-
-
-function CustomTabPanel(props) {
- const { children, value, index, ...other } = props;
-
- return (
-
- {value === index && {children}}
-
- );
-}
-
-export default function BasicTabs() {
- const { id } = useParams();
- const [document, setDocument] = useState({});
- const [value, setValue] = React.useState(0);
- const [isLoading, setIsLoading] = useState(true);
-
- useEffect(() => {
- setIsLoading(true);
- fetchInstance('/api/lookup', { query: { id } })
- .then(response => {
- console.log(JSON.stringify(response))
- const doc = response.document;
- setDocument(doc);
- setIsLoading(false);
- })
- .catch(error => {
- console.log(error);
- setIsLoading(false);
- });
-
- }, [id]);
-
- const handleChange = (event, newValue) => {
- setValue(newValue);
- };
-
-
- if (isLoading || !id || Object.keys(document).length === 0) {
- return (
-
- );
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
{document.original_title}
-

-
{document.authors?.join('; ')} - {document.original_publication_year}
-
ISBN {document.isbn}
-
-
{document.ratings_count} Ratings
-
-
-
-
-
- {JSON.stringify(document, null, 2)}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/pages/Details/Details.tsx b/client/src/pages/Details/Details.tsx
new file mode 100644
index 0000000..31e58c6
--- /dev/null
+++ b/client/src/pages/Details/Details.tsx
@@ -0,0 +1,115 @@
+import React, { useState, useEffect } from "react";
+import { useParams } from 'react-router-dom';
+import Rating from '@mui/material/Rating';
+import CircularProgress from '@mui/material/CircularProgress';
+import Tabs from '@mui/material/Tabs';
+import Tab from '@mui/material/Tab';
+import Box from '@mui/material/Box';
+
+import fetchInstance from '../../url-fetch';
+import { Document } from '../../types/models';
+
+import {
+ TabPanel,
+ TabPanelValue,
+ CardBody,
+ ImageContainer,
+ CardTitle,
+ CardText,
+ BoxContent,
+ DetailsBoxParent,
+ DetailsTabBoxHeader,
+ DetailsCustomTabPanelJsonDiv
+} from './styled';
+
+
+interface CustomTabPanelProps {
+ children?: React.ReactNode;
+ value: number;
+ index: number;
+ component?: React.ElementType;
+ [key: string]: any; // For other props
+}
+
+function CustomTabPanel(props: CustomTabPanelProps) {
+ const { children, value, index, ...other } = props;
+
+ return (
+
+ {value === index && {children}}
+
+ );
+}
+
+export default function BasicTabs() {
+ const { id } = useParams();
+ const [document, setDocument] = useState({} as Document);
+ const [value, setValue] = React.useState(0);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ setIsLoading(true);
+ fetchInstance('/api/lookup', { query: { id: id as string } })
+ .then(response => {
+ console.log(JSON.stringify(response))
+ const doc = response.document;
+ setDocument(doc);
+ setIsLoading(false);
+ })
+ .catch(error => {
+ console.log(error);
+ setIsLoading(false);
+ });
+
+ }, [id]);
+
+ const handleChange = (_, newValue) => {
+ setValue(newValue);
+ };
+
+
+ if (isLoading || !id || Object.keys(document).length === 0) {
+ return (
+
+
+ Loading...
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {document.original_title}
+
+ {document.authors?.join('; ')} - {document.original_publication_year}
+ ISBN {document.isbn}
+
+ {document.ratings_count} Ratings
+
+
+
+
+
+
+ {JSON.stringify(document, null, 2)}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/pages/Details/styled.tsx b/client/src/pages/Details/styled.tsx
new file mode 100644
index 0000000..da085b0
--- /dev/null
+++ b/client/src/pages/Details/styled.tsx
@@ -0,0 +1,86 @@
+import { styled } from '@mui/material/styles';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+
+export const DetailsMain = styled(Box)(() => ({
+ paddingTop: '2em',
+ minHeight: '40em',
+}));
+
+export const TabPanel = styled(Box)(() => ({
+ width: '100%',
+ position: 'relative',
+ overflow: 'visible',
+ backgroundColor: '#fff',
+ padding: '1em',
+ boxSizing: 'border-box',
+ minHeight: 'auto',
+}));
+
+export const TabPanelValue = styled(Box)(() => ({
+ padding: 3,
+}));
+
+export const CardBody = styled(Box)(() => ({
+ height: 'inherit',
+ position: 'relative',
+ backgroundColor: '#fff',
+ border: '1px solid #ddd',
+ boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
+ padding: '1em',
+ margin: '1em 0',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ width: '100%',
+ boxSizing: 'border-box',
+}));
+
+export const ImageContainer = styled('img')(() => ({
+ width: '10em',
+ height: 'auto',
+ marginBottom: '1em',
+}));
+
+export const CardTitle = styled(Typography)(() => ({
+ fontSize: '1.2em',
+ fontWeight: 'bold',
+ marginBottom: '0.5em',
+ textAlign: 'center',
+}));
+
+export const CardText = styled(Typography)(() => ({
+ fontSize: '1em',
+ marginBottom: '0.5em',
+ textAlign: 'center',
+}));
+
+export const BoxHeader = styled(Box)(() => ({
+ borderBottom: '1px solid var(--divider-color)',
+ padding: '1em',
+ backgroundColor: '#f5f5f5',
+}));
+
+export const BoxContent = styled(Box)(() => ({
+ padding: '1.5em',
+ backgroundColor: '#ffffff',
+ border: '1px solid #ddd',
+ boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
+}));
+
+export const DetailsBoxParent = styled(Box)(() => ({
+ width: '100%',
+ paddingTop: '25px',
+ paddingLeft: '150px',
+ paddingRight: '150px',
+}));
+
+export const DetailsTabBoxHeader = styled(Box)(() => ({
+ borderBottom: '1px solid',
+ borderColor: 'var(--divider-color)',
+}));
+
+export const DetailsCustomTabPanelJsonDiv = styled(Box)(() => ({
+ textAlign: 'left',
+}));
diff --git a/client/src/pages/Home/Home.css b/client/src/pages/Home/Home.css
deleted file mode 100644
index 5e5d2d4..0000000
--- a/client/src/pages/Home/Home.css
+++ /dev/null
@@ -1,26 +0,0 @@
-.home-search {
- margin: 5em auto;
- display: flex;
- flex-direction: column;
- align-items: center; /* Center child elements horizontally */
-}
-
-.center-container {
- display: flex;
- justify-content: center;
- align-items: center;
- margin-top: 5em;
-}
-
-.logo {
- height: 12em;
- width: auto;
- display: block;
- margin: auto auto 0;
- object-fit: contain; /* Ensures the image maintains its aspect ratio */
- max-width: 100%; /* Prevents the image from exceeding the container's width */
-}
-
-.poweredby {
- text-align: center;
-}
\ No newline at end of file
diff --git a/client/src/pages/Home/Home.jsx b/client/src/pages/Home/Home.jsx
deleted file mode 100644
index 62c8786..0000000
--- a/client/src/pages/Home/Home.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from "react";
-import { useNavigate } from "react-router-dom";
-
-import SearchBar from '../../components/SearchBar/SearchBar';
-
-import "./Home.css";
-import "../../pages/Search/Search.css";
-import logo from '../../images/cognitive_search.jpg';
-
-export default function Home() {
- const navigate = useNavigate();
- const navigateToSearchPage = (q) => {
- if (!q || q === '') {
- q = '*'
- }
- navigate('/search?q=' + q);
- }
-
- return (
-
-
-
-

-
Powered by Azure AI Search
-
-
-
-
- );
-};
diff --git a/client/src/pages/Home/Home.tsx b/client/src/pages/Home/Home.tsx
new file mode 100644
index 0000000..e2f78f3
--- /dev/null
+++ b/client/src/pages/Home/Home.tsx
@@ -0,0 +1,72 @@
+import React, { useState, useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import Typography from '@mui/material/Typography';
+
+import SearchBar from '../../components/SearchBar/SearchBar';
+
+import { HomeSearchContainer, CenterContainer, LogoImage, HomeSearchBar, SearchControlsRow } from './styled';
+import { HomeMain } from '../../App/styled';
+import logo from '../../images/cognitive_search.jpg';
+
+export default function Home(): React.ReactElement {
+ const navigate = useNavigate();
+ const [imageLoaded, setImageLoaded] = useState(false);
+
+ // Prefetch the search page component when the home page loads
+ useEffect(() => {
+ // Prefetch the Search page component
+ const prefetchSearch = () => {
+ const link = document.createElement('link');
+ link.rel = 'prefetch';
+ link.href = '/src/pages/Search/Search.tsx';
+ link.as = 'script';
+ document.head.appendChild(link);
+ };
+
+ // Short delay to prioritize critical resources first
+ const timer = setTimeout(() => {
+ prefetchSearch();
+ }, 2000);
+
+ return () => clearTimeout(timer);
+ }, []);
+
+ const navigateToSearchPage = (q: string): void => {
+ if (!q || q === '') {
+ q = '*'
+ }
+ navigate('/search?q=' + q);
+ }
+
+ const handleImageLoad = (): void => {
+ setImageLoaded(true);
+ };
+
+ return (
+
+
+
+ {/* Add loading="eager" to prioritize image loading and width/height for layout stability */}
+
+
+ {/* New row for poweredby and search bar that takes 80% width */}
+
+ Powered by Azure AI Search
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/pages/Home/styled.tsx b/client/src/pages/Home/styled.tsx
new file mode 100644
index 0000000..2261b55
--- /dev/null
+++ b/client/src/pages/Home/styled.tsx
@@ -0,0 +1,53 @@
+import { styled } from '@mui/material/styles';
+import Box from '@mui/material/Box';
+
+export const HomeSearchContainer = styled(Box)(() => ({
+ margin: '5em auto',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+}));
+
+export const CenterContainer = styled(Box)(() => ({
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginTop: '5em',
+}));
+
+export const LogoImage = styled('img')<{ isLoaded: boolean }>(({ isLoaded }) => ({
+ height: '12em',
+ width: 'auto',
+ display: 'block',
+ margin: 'auto auto 0',
+ objectFit: 'contain',
+ maxWidth: '100%',
+ transition: 'opacity 0.3s ease-in-out',
+ ...(!isLoaded && {
+ opacity: 0.7,
+ filter: 'blur(2px)',
+ backgroundSize: '400% 400%',
+ }),
+ ...(isLoaded && {
+ opacity: 1,
+ filter: 'none',
+ }),
+}));
+
+export const HomeSearchBar = styled(Box)(() => ({
+ textAlign: 'center',
+ display: 'block',
+ margin: 'auto auto 0',
+ width: '100%',
+ maxWidth: '800px', // Set a max width for large screens
+ paddingRight: '20px',
+ paddingLeft: '20px',
+}));
+
+export const SearchControlsRow = styled(Box)(() => ({
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ width: '100%',
+ marginTop: '1em',
+}));
diff --git a/client/src/pages/Search/Search.css b/client/src/pages/Search/Search.css
deleted file mode 100644
index 17fe932..0000000
--- a/client/src/pages/Search/Search.css
+++ /dev/null
@@ -1,56 +0,0 @@
-.main--search {
- display: flex;
- flex-direction: column;
-}
-.row {
- display: flex;
- flex-wrap: wrap;
-}
-
-.search-bar-column {
- flex: 1;
- min-width: 300px; /* Adjust as needed */
-}
-
-.search-bar-results {
- flex: 2;
- min-width: 300px;
-}
-
-@media (max-width: 768px) {
- .row {
- flex-direction: column;
- }
-
-}
-
-
-.sui-layout-header {
- background-color: #0078d7;
- color: #eee;
-}
-.sui-search-box__submit {
- background: linear-gradient(rgb(60, 226, 102), rgb(34, 151, 57));
- letter-spacing: 0.1em;
-}
-.sui-search-box__submit:hover {
- background: linear-gradient(rgb(34, 151, 57), rgb(60, 226, 102));
-}
-
-.pager-style {
- margin-left: auto;
- margin-right: auto;
- max-width: fit-content;
-}
-
-/* Adjust the width of the search-bar to make it shorter */
-.search-bar-column-container {
-
- width: 100%; /* Adjusted width to make it shorter */
- margin: 0 auto; /* Center the search bar */
- margin-top: 10px;
-}
-.search-results-container {
- flex: 0 0 100%; /* Takes up 75% of the row */
- max-width: 100%;
-}
\ No newline at end of file
diff --git a/client/src/pages/Search/Search.jsx b/client/src/pages/Search/Search.jsx
deleted file mode 100644
index 5b7dabd..0000000
--- a/client/src/pages/Search/Search.jsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import React, { useEffect, useState, Suspense } from 'react';
-import fetchInstance from '../../url-fetch';
-import CircularProgress from '@mui/material/CircularProgress';
-import { useLocation, useNavigate } from "react-router-dom";
-
-import Results from '../../components/Results/Results';
-import Pager from '../../components/Pager/Pager';
-import Facets from '../../components/Facets/Facets';
-import SearchBar from '../../components/SearchBar/SearchBar';
-
-import "./Search.css";
-
-export default function Search() {
-
- let location = useLocation();
- const navigate = useNavigate();
-
- const [results, setResults] = useState([]);
- const [resultCount, setResultCount] = useState(0);
- const [currentPage, setCurrentPage] = useState(1);
- const [q, setQ] = useState(new URLSearchParams(location.search).get('q') ?? "*");
- const [top] = useState(new URLSearchParams(location.search).get('top') ?? 8);
- const [skip, setSkip] = useState(new URLSearchParams(location.search).get('skip') ?? 0);
- const [filters, setFilters] = useState([]);
- const [facets, setFacets] = useState({});
- const [isLoading, setIsLoading] = useState(true);
-
- let resultsPerPage = top;
-
- // Handle page changes in a controlled manner
- function handlePageChange(newPage) {
- setCurrentPage(newPage);
- }
-
- // Calculate skip value and fetch results when relevant parameters change
- useEffect(() => {
- // Calculate skip based on current page
- const calculatedSkip = (currentPage - 1) * top;
-
- // Only update if skip has actually changed
- if (calculatedSkip !== skip) {
- setSkip(calculatedSkip);
- return; // Skip the fetch since skip will change and trigger another useEffect
- }
-
- // Proceed with fetch
- setIsLoading(true);
-
- const body = {
- q: q,
- top: top,
- skip: skip,
- filters: filters
- };
-
-
- fetchInstance('/api/search', { body, method: 'POST' })
- .then(response => {
- setResults(response.results);
- setFacets(response.facets);
- setResultCount(response.count);
- setIsLoading(false);
- })
- .catch(error => {
- console.log(error);
- setIsLoading(false);
- });
- }, [q, top, skip, filters, currentPage]);
-
- // pushing the new search term to history when q is updated
- // allows the back button to work as expected when coming back from the details page
- useEffect(() => {
- navigate('/search?q=' + q);
- setCurrentPage(1);
- setFilters([]);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [q]);
-
-
- let postSearchHandler = (searchTerm) => {
- setQ(searchTerm);
- }
-
-
- // filters should be applied across entire result set,
- // not just within the current page
- const updateFilterHandler = (newFilters) => {
-
- // Reset paging
- setSkip(0);
- setCurrentPage(1);
-
- // Set filters
- setFilters(newFilters);
- };
-
- return (
-
-
-
-
- {isLoading ? (
-
-
-
- ) : (
-
- )}
-
-
-
- );
-}
diff --git a/client/src/pages/Search/Search.tsx b/client/src/pages/Search/Search.tsx
new file mode 100644
index 0000000..da33c1d
--- /dev/null
+++ b/client/src/pages/Search/Search.tsx
@@ -0,0 +1,150 @@
+import React, { useEffect, useState} from 'react';
+import fetchInstance from '../../url-fetch';
+import CircularProgress from '@mui/material/CircularProgress';
+import { useLocation, useNavigate } from "react-router-dom";
+import Grid from '@mui/material/Grid';
+import Box from '@mui/material/Box';
+import Container from '@mui/material/Container';
+import { Facet } from '../../types/models';
+import { SearchResponse, SearchResultDocument } from '../../types/api';
+
+import Results from '../../components/Results/Results';
+import Pager from '../../components/Pager';
+import Facets from '../../components/Facets/Facets';
+import SearchBar from '../../components/SearchBar/SearchBar';
+
+import {
+ SearchMain,
+ SearchBarColumn,
+ SearchBarResults,
+ SearchBarColumnContainer,
+ SearchResultsContainer,
+ PagerStyle
+} from './styled';
+
+export default function Search(): React.ReactElement {
+
+ let location = useLocation();
+ const navigate = useNavigate();
+
+ const [results, setResults] = useState([]);
+
+ const [q, setQ] = useState(new URLSearchParams(location.search).get('q') ?? "*");
+
+ const [resultCount, setResultCount] = useState(0);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [top] = useState(Number(new URLSearchParams(location.search).get('top')) || 8);
+ const [skip, setSkip] = useState(Number(new URLSearchParams(location.search).get('skip')) || 0);
+ const [filters, setFilters] = useState([]);
+ const [facets, setFacets] = useState>({});
+ const [isLoading, setIsLoading] = useState(true);
+
+ let resultsPerPage = top;
+
+ // Handle page changes in a controlled manner
+ function handlePageChange(newPage: number): void {
+ setCurrentPage(newPage);
+ }
+
+ // Calculate skip value and fetch results when relevant parameters change
+ useEffect(() => {
+ // Calculate skip based on current page
+ const calculatedSkip = (currentPage - 1) * top;
+
+ // Only update if skip has actually changed
+ if (calculatedSkip !== skip) {
+ setSkip(calculatedSkip);
+ return; // Skip the fetch since skip will change and trigger another useEffect
+ }
+
+ // Proceed with fetch
+ setIsLoading(true);
+
+ const body = {
+ q: q,
+ top: top,
+ skip: skip,
+ filters: filters
+ };
+
+
+ fetchInstance('/api/search', { body, method: 'POST' })
+ .then(apiResponse => {
+ // Map API response to our new interface
+ const response: SearchResponse = {
+ count: apiResponse.count || 0,
+ facets: apiResponse.facets || {},
+ // Map existing results or documents to our new searchResults property
+ searchResults: (apiResponse.results || apiResponse.documents || []) as SearchResultDocument[],
+ skip: apiResponse.skip,
+ top: apiResponse.top
+ };
+
+ setResults(response.searchResults);
+ setFacets(response.facets);
+ setResultCount(response.count);
+ setIsLoading(false);
+ })
+ .catch(error => {
+ console.error('Search error:', error);
+ setIsLoading(false);
+ });
+ }, [q, top, skip, filters, currentPage]);
+
+ // pushing the new search term to history when q is updated
+ // allows the back button to work as expected when coming back from the details page
+ useEffect(() => {
+ navigate('/search?q=' + q);
+ setCurrentPage(1);
+ setFilters([]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [q]);
+
+
+ let postSearchHandler = (searchTerm: string): void => {
+ setQ(searchTerm);
+ }
+
+
+ // filters should be applied across entire result set,
+ // not just within the current page
+ const updateFilterHandler = (newFilters: string[]): void => {
+
+ // Reset paging
+ setSkip(0);
+ setCurrentPage(1);
+
+ // Set filters
+ setFilters(newFilters);
+ };
+
+ return (
+
+ {/* Added horizontal padding and top margin */}
+
+
+
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/client/src/pages/Search/styled.tsx b/client/src/pages/Search/styled.tsx
new file mode 100644
index 0000000..f7a2109
--- /dev/null
+++ b/client/src/pages/Search/styled.tsx
@@ -0,0 +1,42 @@
+import { styled } from '@mui/material/styles';
+import Box from '@mui/material/Box';
+
+export const SearchMain = styled(Box)(() => ({
+ display: 'flex',
+ flexDirection: 'column',
+}));
+
+export const Row = styled(Box)(() => ({
+ display: 'flex',
+ flexWrap: 'wrap',
+ '@media (max-width: 768px)': {
+ flexDirection: 'column',
+ },
+}));
+
+export const SearchBarColumn = styled(Box)(() => ({
+ flex: 1,
+ minWidth: '300px',
+}));
+
+export const SearchBarResults = styled(Box)(() => ({
+ flex: 2,
+ minWidth: '300px',
+}));
+
+export const PagerStyle = styled(Box)(() => ({
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ maxWidth: 'fit-content',
+}));
+
+export const SearchBarColumnContainer = styled(Box)(() => ({
+ width: '100%',
+ margin: '0 auto',
+ marginTop: '10px',
+}));
+
+export const SearchResultsContainer = styled(Box)(() => ({
+ flex: '0 0 100%',
+ maxWidth: '100%',
+}));
diff --git a/client/src/theme.ts b/client/src/theme.ts
new file mode 100644
index 0000000..2f69549
--- /dev/null
+++ b/client/src/theme.ts
@@ -0,0 +1,156 @@
+import { createTheme } from '@mui/material/styles';
+import { debugStyles } from './debug'; // Import debug styles
+
+// Create a theme instance
+const theme = createTheme({
+ typography: {
+ fontFamily: 'system-ui, Avenir, Helvetica, Arial, sans-serif',
+ fontSize: 14,
+ fontWeightLight: 300,
+ fontWeightRegular: 400,
+ fontWeightMedium: 500,
+ fontWeightBold: 700,
+ },
+ palette: {
+ primary: {
+ main: '#646cff', // From your link color
+ dark: '#535bf2', // From your link hover color
+ light: '#747bff', // From your light theme link hover
+ },
+ secondary: {
+ main: '#0078d4', // Azure blue
+ },
+ background: {
+ default: '#ffffff',
+ paper: '#ffffff',
+ },
+ text: {
+ primary: '#213547', // From your light theme color
+ secondary: '#6e6e6e',
+ },
+ action: {
+ hover: 'rgba(100, 108, 255, 0.08)', // Light blue hover effect
+ },
+ },
+ components: {
+ MuiCssBaseline: {
+ styleOverrides: {
+ html: {
+ WebkitFontSmoothing: 'antialiased',
+ MozOsxFontSmoothing: 'grayscale',
+ textRendering: 'optimizeLegibility',
+ },
+ body: {
+ fontFamily: 'system-ui, Avenir, Helvetica, Arial, sans-serif',
+ lineHeight: 1.5,
+ margin: 0,
+ padding: 0,
+ minWidth: '320px',
+ },
+ h1: {
+ fontSize: '3.2em',
+ lineHeight: 1.1,
+ },
+ a: {
+ fontWeight: 500,
+ color: '#646cff',
+ textDecoration: 'inherit',
+ '&:hover': {
+ color: '#747bff',
+ },
+ },
+ button: {
+ borderRadius: '8px',
+ border: '1px solid transparent',
+ padding: '0.6em 1.2em',
+ fontSize: '1em',
+ fontWeight: 500,
+ fontFamily: 'inherit',
+ backgroundColor: '#f9f9f9',
+ cursor: 'pointer',
+ transition: 'border-color 0.25s',
+ '&:hover': {
+ borderColor: '#646cff',
+ },
+ '&:focus, &:focus-visible': {
+ outline: '4px auto -webkit-focus-ring-color',
+ },
+ },
+ },
+ },
+ },
+});
+
+// Define global styles that can be used with MUI's GlobalStyles component
+export const globalStyles = {
+ ':root': {
+ // Base font styles
+ fontFamily: 'system-ui, Avenir, Helvetica, Arial, sans-serif',
+ lineHeight: 1.5,
+ fontWeight: 400,
+
+ // Text rendering optimizations
+ fontSynthesis: 'none',
+ textRendering: 'optimizeLegibility',
+ WebkitFontSmoothing: 'antialiased',
+ MozOsxFontSmoothing: 'grayscale',
+
+ // Light theme colors
+ color: '#213547',
+ backgroundColor: '#ffffff',
+ },
+
+ // Debug styles imported from debug.js
+ // To enable/disable, just change DEBUG_MODE in debug.js
+ ...debugStyles,
+
+ // Link styles
+ 'a': {
+ fontWeight: 500,
+ color: '#646cff',
+ textDecoration: 'inherit',
+ },
+ 'a:hover': {
+ color: '#747bff',
+ },
+
+ // Body styles
+ 'body': {
+ margin: 0,
+ padding: 0,
+ minWidth: '320px',
+ fontFamily: 'system-ui, Avenir, Helvetica, Arial, sans-serif',
+ },
+
+ // Heading styles
+ 'h1': {
+ fontSize: '3.2em',
+ lineHeight: 1.1,
+ },
+
+ // Button styles
+ 'button': {
+ borderRadius: '8px',
+ border: '1px solid transparent',
+ padding: '0.6em 1.2em',
+ fontSize: '1em',
+ fontWeight: 500,
+ fontFamily: 'inherit',
+ backgroundColor: '#f9f9f9', // Light theme button color
+ cursor: 'pointer',
+ transition: 'border-color 0.25s',
+ },
+ 'button:hover': {
+ borderColor: '#646cff',
+ },
+ 'button:focus, button:focus-visible': {
+ outline: '4px auto -webkit-focus-ring-color',
+ },
+
+ // Box sizing for all elements
+ '*, *::before, *::after': {
+ boxSizing: 'border-box',
+ },
+};
+
+export default theme;
diff --git a/client/src/types/api.ts b/client/src/types/api.ts
new file mode 100644
index 0000000..6cfcb26
--- /dev/null
+++ b/client/src/types/api.ts
@@ -0,0 +1,41 @@
+// Types for API requests and responses
+
+import { Document, Facet } from './models';
+
+export interface SearchResultDocument {
+ score: number;
+ document: Document;
+}
+
+export interface SearchRequest {
+ q: string;
+ top?: number;
+ skip?: number;
+ facets?: string[];
+ selectedFacets?: Record;
+}
+
+export interface SuggestRequest {
+ q: string;
+ top?: number;
+ suggester?: string;
+}
+
+export interface SearchResponse {
+ count: number;
+ facets: Record;
+ searchResults: SearchResultDocument[];
+ skip?: number;
+ top?: number;
+}
+
+export interface SuggestResponse {
+ suggestions: {
+ text: string;
+ [key: string]: any;
+ }[];
+}
+
+export interface LookupResponse {
+ document: Document;
+}
diff --git a/client/src/types/global.d.ts b/client/src/types/global.d.ts
new file mode 100644
index 0000000..360c2d1
--- /dev/null
+++ b/client/src/types/global.d.ts
@@ -0,0 +1,44 @@
+// This file contains global type declarations for the project
+
+// Declare modules for image imports
+declare module '*.jpg' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.jpeg' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.png' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.svg' {
+ import * as React from 'react';
+ export const ReactComponent: React.FC>;
+ const src: string;
+ export default src;
+}
+
+declare module '*.ico' {
+ const src: string;
+ export default src;
+}
+
+// Environment variables
+interface ImportMetaEnv {
+ readonly VITE_API_URL?: string;
+ readonly VITE_REACT_APP_BACKEND_URL?: string;
+ readonly DEV: boolean;
+ readonly MODE: string;
+ readonly SSR: boolean;
+ readonly BASE_URL: string;
+ // Add any other environment variables used in your app
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/client/src/types/index.ts b/client/src/types/index.ts
new file mode 100644
index 0000000..80e320f
--- /dev/null
+++ b/client/src/types/index.ts
@@ -0,0 +1,4 @@
+// Index file exporting all types
+export * from './models';
+export * from './api';
+export * from './props';
diff --git a/client/src/types/models.ts b/client/src/types/models.ts
new file mode 100644
index 0000000..fccf630
--- /dev/null
+++ b/client/src/types/models.ts
@@ -0,0 +1,44 @@
+// Type definitions for the application models
+
+export interface Document {
+ id: string;
+ authors?: string[];
+ average_rating?: string | number;
+ best_book_id?: number;
+ books_count?: number;
+ goodreads_book_id?: number;
+ image_url?: string;
+ isbn?: string;
+ isbn13?: string;
+ language_code?: string;
+ original_publication_year?: string | number;
+ original_title?: string;
+ ratings_1?: number;
+ ratings_2?: number;
+ ratings_3?: number;
+ ratings_4?: number;
+ ratings_5?: number;
+ ratings_count?: number;
+ small_image_url?: string;
+ title?: string;
+ work_id?: number;
+ work_ratings_count?: number;
+ work_text_reviews_count?: number;
+ [key: string]: any; // Allow for additional dynamic properties
+}
+
+export interface SearchResult {
+ document: Document;
+ score?: number;
+}
+
+export interface FacetValue {
+ value: string;
+ count: number;
+ selected: boolean;
+}
+
+export interface Facet {
+ fieldName: string;
+ values: FacetValue[];
+}
diff --git a/client/src/types/props.ts b/client/src/types/props.ts
new file mode 100644
index 0000000..3de8b75
--- /dev/null
+++ b/client/src/types/props.ts
@@ -0,0 +1,40 @@
+// Types for React component props
+import { Document, Facet } from './models';
+import { SearchResultDocument } from './api';
+
+export interface SearchBarProps {
+ postSearchHandler: (query: string) => void;
+ query?: string;
+ width?: string | number;
+}
+
+export interface ResultsProps {
+ query?: string;
+ searchResultDocuments: SearchResultDocument[];
+ count?: number;
+ skip?: number;
+ top?: number;
+}
+
+export interface ResultProps {
+ document: Document;
+}
+
+export interface PagerProps {
+ pageCount: number;
+ currentPage: number;
+ onPageChange: (page: number) => void;
+}
+
+export interface FacetsProps {
+ facets?: Record;
+ onFacetValueSelection: (fieldName: string, value: string, selected: boolean) => void;
+ selectedFacets: Record;
+}
+
+export interface CheckboxFacetProps {
+ fieldName: string;
+ values: Array<{value: string; count: number; selected: boolean}>;
+ onSelection: (fieldName: string, value: string, selected: boolean) => void;
+ selectedFacets: Record;
+}
diff --git a/client/src/url-fetch.js b/client/src/url-fetch.ts
similarity index 64%
rename from client/src/url-fetch.js
rename to client/src/url-fetch.ts
index eb09924..640abe8 100644
--- a/client/src/url-fetch.js
+++ b/client/src/url-fetch.ts
@@ -5,14 +5,21 @@ const baseURL = import.meta.env.DEV
console.log(`baseURL = ${baseURL}`);
console.log(`Environment: ${import.meta.env.MODE}`);
-function buildQueryString(params) {
+function buildQueryString(params: Record): string {
return Object.keys(params)
- .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key].toString())}`)
.join('&');
}
-async function fetchInstance(url, { query = {}, body = null, headers = {}, method = 'GET' } = {}) {
- const queryString = buildQueryString(query);
+interface FetchOptions {
+ query?: Record;
+ body?: any;
+ headers?: Record;
+ method?: string;
+}
+
+async function fetchInstance(url: string, { query = {}, body = null, headers = {}, method = 'GET' }: FetchOptions = {}): Promise {
+ const queryString = buildQueryString(query as Record);
// Handle empty baseURL for production (relative URLs)
const fullUrl = baseURL ? `${baseURL}${url}${queryString ? `?${queryString}` : ''}` : `${url}${queryString ? `?${queryString}` : ''}`;
@@ -26,7 +33,7 @@ async function fetchInstance(url, { query = {}, body = null, headers = {}, metho
});
if (response.ok || (response.status >= 200 && response.status < 400)) {
- return await response.json();
+ return await response.json() as T;
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
diff --git a/client/tsconfig.json b/client/tsconfig.json
new file mode 100644
index 0000000..ad52ac5
--- /dev/null
+++ b/client/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "outDir": "build",
+ "strict": true,
+ "noImplicitAny": false,
+ "noUnusedLocals": false,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "esModuleInterop": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json
new file mode 100644
index 0000000..165a9ba
--- /dev/null
+++ b/client/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/client/vite.config.js b/client/vite.config.js
deleted file mode 100644
index 913f14b..0000000
--- a/client/vite.config.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-
-// https://vite.dev/config/
-export default defineConfig({
- plugins: [react()],
- build: {
- outDir: 'build'
- }
-})
diff --git a/client/vite.config.ts b/client/vite.config.ts
new file mode 100644
index 0000000..3e777ab
--- /dev/null
+++ b/client/vite.config.ts
@@ -0,0 +1,23 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ outDir: 'build',
+ sourcemap: true,
+ rollupOptions: {
+ output: {
+ manualChunks: {
+ 'vendor': ['react', 'react-dom', 'react-router-dom'],
+ 'mui': ['@mui/material', '@mui/icons-material', '@emotion/react', '@emotion/styled']
+ }
+ }
+ }
+ },
+ server: {
+ port: 3000,
+ host: true
+ }
+})