This commit is contained in:
seb
2025-07-02 12:49:06 +02:00
commit edbd56f6a9
123 changed files with 32598 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
import React, { Component } from 'react';
import Box from '@mui/material/Box';
import Badge from '@mui/material/Badge';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import Divider from '@mui/material/Divider';
import Typography from '@mui/material/Typography';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import CloseIcon from '@mui/icons-material/Close';
import { useNavigate } from 'react-router-dom';
import LoginComponent from '../LoginComponent.js';
import CartDropdown from '../CartDropdown.js';
import { isUserLoggedIn } from '../LoginComponent.js';
function getBadgeNumber() {
let count = 0;
if (Array.isArray(window.cart)) for (const item of window.cart) {
if (item.quantity) count += item.quantity;
}
return count;
}
class ButtonGroup extends Component {
constructor(props) {
super(props);
this.state = {
isCartOpen: false,
badgeNumber: getBadgeNumber()
};
this.isUpdatingFromSocket = false; // @note Flag to prevent socket loop
}
componentDidMount() {
this.cart = () => {
// @note Only emit if socket exists, is connected, AND the update didn't come from socket
if (this.props.socket && this.props.socket.connected && !this.isUpdatingFromSocket) {
this.props.socket.emit('updateCart', window.cart);
}
this.setState({
badgeNumber: getBadgeNumber()
});
};
window.addEventListener('cart', this.cart);
// Add event listener for the toggle-cart event from AddToCartButton
this.toggleCartListener = () => this.toggleCart();
window.addEventListener('toggle-cart', this.toggleCartListener);
// Add socket listeners if socket is available and connected
this.addSocketListeners();
}
componentDidUpdate(prevProps) {
// Handle socket connection changes
const wasConnected = prevProps.socket && prevProps.socket.connected;
const isNowConnected = this.props.socket && this.props.socket.connected;
if (!wasConnected && isNowConnected) {
// Socket just connected, add listeners
this.addSocketListeners();
} else if (wasConnected && !isNowConnected) {
// Socket just disconnected, remove listeners
this.removeSocketListeners();
}
}
componentWillUnmount() {
window.removeEventListener('cart', this.cart);
window.removeEventListener('toggle-cart', this.toggleCartListener);
this.removeSocketListeners();
}
addSocketListeners = () => {
if (this.props.socket && this.props.socket.connected) {
// Remove existing listeners first to avoid duplicates
this.removeSocketListeners();
this.props.socket.on('cartUpdated', this.handleCartUpdated);
}
}
removeSocketListeners = () => {
if (this.props.socket) {
this.props.socket.off('cartUpdated', this.handleCartUpdated);
}
}
handleCartUpdated = (id,user,cart) => {
const storedUser = sessionStorage.getItem('user');
if (storedUser) {
try {
const parsedUser = JSON.parse(storedUser);
if(user && parsedUser &&user.email == parsedUser.email){
// @note Set flag before updating cart to prevent socket loop
this.isUpdatingFromSocket = true;
window.cart = cart;
this.setState({
badgeNumber: getBadgeNumber()
});
// @note Reset flag after a short delay to allow for any synchronous events
setTimeout(() => {
this.isUpdatingFromSocket = false;
}, 0);
}
} catch (error) {
console.error('Error parsing user from sessionStorage:', error);
}
}
}
toggleCart = () => {
this.setState(prevState => ({
isCartOpen: !prevState.isCartOpen
}));
}
render() {
const { socket, navigate } = this.props;
const { isCartOpen } = this.state;
const cartItems = Array.isArray(window.cart) ? window.cart : [];
return (
<Box sx={{ display: 'flex', gap: { xs: 0.5, sm: 1 } }}>
<LoginComponent socket={socket} />
<IconButton
color="inherit"
onClick={this.toggleCart}
sx={{ ml: 1 }}
>
<Badge badgeContent={this.state.badgeNumber} color="error">
<ShoppingCartIcon />
</Badge>
</IconButton>
<Drawer
anchor="left"
open={isCartOpen}
onClose={this.toggleCart}
disableScrollLock={true}
>
<Box sx={{ width: 420, p: 2 }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 1
}}
>
<IconButton
onClick={this.toggleCart}
size="small"
sx={{
bgcolor: 'primary.main',
color: 'primary.contrastText',
'&:hover': {
bgcolor: 'primary.dark',
}
}}
>
<CloseIcon />
</IconButton>
<Typography variant="h6">Warenkorb</Typography>
</Box>
<Divider sx={{ mb: 2 }} />
<CartDropdown cartItems={cartItems} socket={socket} onClose={this.toggleCart} onCheckout={()=>{
/*open the Drawer inside <LoginComponent */
if (isUserLoggedIn().isLoggedIn) {
this.toggleCart(); // Close the cart drawer
navigate('/profile');
} else if (window.openLoginDrawer) {
window.openLoginDrawer(); // Call global function to open login drawer
this.toggleCart(); // Close the cart drawer
} else {
console.error('openLoginDrawer function not available');
}
}}/>
</Box>
</Drawer>
</Box>
);
}
}
// Wrapper for ButtonGroup to provide navigate function
const ButtonGroupWithRouter = (props) => {
const navigate = useNavigate();
return <ButtonGroup {...props} navigate={navigate} />;
};
export default ButtonGroupWithRouter;

View File

@@ -0,0 +1,481 @@
import React, { Component, Profiler } from "react";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import { Link } from "react-router-dom";
import HomeIcon from "@mui/icons-material/Home";
class CategoryList extends Component {
findCategoryById = (category, targetId) => {
if (!category) return null;
if (category.seoName === targetId) {
return category;
}
if (category.children) {
for (let child of category.children) {
const found = this.findCategoryById(child, targetId);
if (found) return found;
}
}
return null;
};
getPathToCategory = (category, targetId, currentPath = []) => {
if (!category) return null;
const newPath = [...currentPath, category];
if (category.seoName === targetId) {
return newPath;
}
if (category.children) {
for (let child of category.children) {
const found = this.getPathToCategory(child, targetId, newPath);
if (found) return found;
}
}
return null;
};
constructor(props) {
super(props);
// Check for cached data during SSR/initial render
let initialState = {
categoryTree: null,
level1Categories: [], // Children of category 209 (Home) - always shown
level2Categories: [], // Children of active level 1 category
level3Categories: [], // Children of active level 2 category
activePath: [], // Array of active category objects for each level
fetchedCategories: false,
};
// Try to get cached data for SSR
try {
// @note Check both global.window (SSR) and window (browser) for cache
const productCache = (typeof global !== "undefined" && global.window && global.window.productCache) ||
(typeof window !== "undefined" && window.productCache);
if (productCache) {
const cacheKey = "categoryTree_209";
const cachedData = productCache[cacheKey];
if (cachedData && cachedData.categoryTree) {
const { categoryTree, timestamp } = cachedData;
const cacheAge = Date.now() - timestamp;
const tenMinutes = 10 * 60 * 1000;
// Use cached data if it's fresh
if (cacheAge < tenMinutes) {
initialState.categoryTree = categoryTree;
initialState.fetchedCategories = true;
// Process category tree to set up navigation
const level1Categories =
categoryTree && categoryTree.id === 209
? categoryTree.children || []
: [];
initialState.level1Categories = level1Categories;
// Process active category path if needed
if (props.activeCategoryId) {
const activeCategory = this.findCategoryById(
categoryTree,
props.activeCategoryId
);
if (activeCategory) {
const pathToActive = this.getPathToCategory(
categoryTree,
props.activeCategoryId
);
initialState.activePath = pathToActive
? pathToActive.slice(1)
: [];
if (initialState.activePath.length >= 1) {
const level1Category = initialState.activePath[0];
initialState.level2Categories = level1Category.children || [];
}
if (initialState.activePath.length >= 2) {
const level2Category = initialState.activePath[1];
initialState.level3Categories = level2Category.children || [];
}
}
}
}
}
}
} catch (err) {
console.error("Error reading cache in constructor:", err);
}
this.state = initialState;
}
componentDidMount() {
this.fetchCategories();
}
componentDidUpdate(prevProps) {
// Handle socket connection changes
const wasConnected = prevProps.socket && prevProps.socket.connected;
const isNowConnected = this.props.socket && this.props.socket.connected;
if (!wasConnected && isNowConnected && !this.state.fetchedCategories) {
// Socket just connected and we haven't fetched categories yet
this.setState(
{
fetchedCategories: false,
},
() => {
this.fetchCategories();
}
);
}
// If activeCategoryId changes, update subcategories
if (
prevProps.activeCategoryId !== this.props.activeCategoryId &&
this.state.categoryTree
) {
this.processCategoryTree(this.state.categoryTree);
}
}
fetchCategories = () => {
const { socket } = this.props;
if (!socket || !socket.connected) {
// Socket not connected yet, but don't show error immediately on first load
// The componentDidUpdate will retry when socket connects
console.log("Socket not connected yet, waiting for connection to fetch categories");
return;
}
if (this.state.fetchedCategories) {
//console.log('Categories already fetched, skipping');
return;
}
// Initialize global cache object if it doesn't exist
// @note Handle both SSR (global.window) and browser (window) environments
const windowObj = (typeof global !== "undefined" && global.window) ||
(typeof window !== "undefined" && window);
if (windowObj && !windowObj.productCache) {
windowObj.productCache = {};
}
// Check if we have a valid cache in the global object
try {
const cacheKey = "categoryTree_209";
const cachedData = windowObj && windowObj.productCache ? windowObj.productCache[cacheKey] : null;
if (cachedData) {
const { categoryTree, fetching } = cachedData;
//const cacheAge = Date.now() - timestamp;
//const tenMinutes = 10 * 60 * 1000; // 10 minutes in milliseconds
// If data is currently being fetched, wait for it
if (fetching) {
//console.log('CategoryList: Data is being fetched, waiting...');
const checkInterval = setInterval(() => {
const currentCache = windowObj && windowObj.productCache ? windowObj.productCache[cacheKey] : null;
if (currentCache && !currentCache.fetching) {
clearInterval(checkInterval);
if (currentCache.categoryTree) {
this.processCategoryTree(currentCache.categoryTree);
}
}
}, 100);
return;
}
// If cache is less than 10 minutes old, use it
if (/*cacheAge < tenMinutes &&*/ categoryTree) {
//console.log('Using cached category tree, age:', Math.round(cacheAge/1000), 'seconds');
// Defer processing to next tick to avoid blocking
//setTimeout(() => {
this.processCategoryTree(categoryTree);
//}, 0);
//return;
}
}
} catch (err) {
console.error("Error reading from cache:", err);
}
// Mark as being fetched to prevent concurrent calls
const cacheKey = "categoryTree_209";
if (windowObj && windowObj.productCache) {
windowObj.productCache[cacheKey] = {
fetching: true,
timestamp: Date.now(),
};
}
this.setState({ fetchedCategories: true });
//console.log('CategoryList: Fetching categories from socket');
socket.emit("categoryList", { categoryId: 209 }, (response) => {
if (response && response.categoryTree) {
//console.log('Category tree received:', response.categoryTree);
// Store in global cache with timestamp
try {
const cacheKey = "categoryTree_209";
if (windowObj && windowObj.productCache) {
windowObj.productCache[cacheKey] = {
categoryTree: response.categoryTree,
timestamp: Date.now(),
fetching: false,
};
}
} catch (err) {
console.error("Error writing to cache:", err);
}
this.processCategoryTree(response.categoryTree);
} else {
try {
const cacheKey = "categoryTree_209";
if (windowObj && windowObj.productCache) {
windowObj.productCache[cacheKey] = {
categoryTree: null,
timestamp: Date.now(),
};
}
} catch (err) {
console.error("Error writing to cache:", err);
}
this.setState({
categoryTree: null,
level1Categories: [],
level2Categories: [],
level3Categories: [],
activePath: [],
});
}
});
};
processCategoryTree = (categoryTree) => {
// Level 1 categories are always the children of category 209 (Home)
const level1Categories =
categoryTree && categoryTree.id === 209
? categoryTree.children || []
: [];
// Build the navigation path and determine what to show at each level
let level2Categories = [];
let level3Categories = [];
let activePath = [];
if (this.props.activeCategoryId) {
const activeCategory = this.findCategoryById(
categoryTree,
this.props.activeCategoryId
);
if (activeCategory) {
// Build the path from root to active category
const pathToActive = this.getPathToCategory(
categoryTree,
this.props.activeCategoryId
);
activePath = pathToActive.slice(1); // Remove root (209) from path
// Determine what to show at each level based on the path depth
if (activePath.length >= 1) {
// Show children of the level 1 category
const level1Category = activePath[0];
level2Categories = level1Category.children || [];
}
if (activePath.length >= 2) {
// Show children of the level 2 category
const level2Category = activePath[1];
level3Categories = level2Category.children || [];
}
}
}
this.setState({
categoryTree,
level1Categories,
level2Categories,
level3Categories,
activePath,
fetchedCategories: true,
});
};
render() {
const { level1Categories, level2Categories, level3Categories, activePath } =
this.state;
const renderCategoryRow = (categories, level = 1) => (
<Box
sx={{
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
flexWrap: "nowrap",
overflowX: "auto",
py: 0.5, // Add vertical padding to prevent border clipping
"&::-webkit-scrollbar": {
display: "none",
},
scrollbarWidth: "none",
msOverflowStyle: "none",
}}
>
{level === 1 && (
<Button
component={Link}
to="/"
color="inherit"
size="small"
aria-label="Zur Startseite"
sx={{
fontSize: "0.75rem",
fontWeight: "normal",
textTransform: "none",
whiteSpace: "nowrap",
opacity: 0.9,
mx: 0.5,
my: 0.25, // Add consistent vertical margin to account for borders
minWidth: "auto",
border: "2px solid transparent", // Always have border space
borderRadius: 1, // Always have border radius
...(this.props.activeCategoryId === null && {
boxShadow: "0 4px 12px rgba(0,0,0,0.5)",
transform: "translateY(-2px)",
bgcolor: "rgba(255,255,255,0.25)",
borderColor: "rgba(255,255,255,0.6)", // Change border color instead of adding border
fontWeight: "bold",
opacity: 1,
}),
"&:hover": {
opacity: 1,
bgcolor: "rgba(255,255,255,0.15)",
transform: "translateY(-1px)",
boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
},
}}
>
<HomeIcon sx={{ fontSize: "1rem" }} />
</Button>
)}
{this.state.fetchedCategories && categories.length > 0 ? (
<>
{categories.map((category) => {
// Determine if this category is active at this level
const isActiveAtThisLevel =
activePath[level - 1] &&
activePath[level - 1].id === category.id;
return (
<Button
key={category.id}
component={Link}
to={`/Kategorie/${category.seoName}`}
color="inherit"
size="small"
sx={{
fontSize: "0.75rem",
fontWeight: "normal",
textTransform: "none",
whiteSpace: "nowrap",
opacity: 0.9,
mx: 0.5,
my: 0.25, // Add consistent vertical margin to account for borders
border: "2px solid transparent", // Always have border space
borderRadius: 1, // Always have border radius
...(isActiveAtThisLevel && {
boxShadow: "0 4px 12px rgba(0,0,0,0.5)",
transform: "translateY(-2px)",
bgcolor: "rgba(255,255,255,0.25)",
borderColor: "rgba(255,255,255,0.6)", // Change border color instead of adding border
fontWeight: "bold",
opacity: 1,
}),
"&:hover": {
opacity: 1,
bgcolor: "rgba(255,255,255,0.15)",
transform: "translateY(-1px)",
boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
},
}}
>
{category.name}
</Button>
);
})}
</>
) : (
level === 1 && (
<Typography
variant="caption"
color="inherit"
sx={{
display: "inline-flex",
alignItems: "center",
height: "30px", // Match small button height
px: 1,
fontSize: "0.75rem",
opacity: 0.9,
}}
>
&nbsp;
</Typography>
)
)}
</Box>
);
const onRenderCallback = (id, phase, actualDuration) => {
if (actualDuration > 50) {
console.warn(
`CategoryList render took ${actualDuration}ms in ${phase} phase`
);
}
};
return (
<Profiler id="CategoryList" onRender={onRenderCallback}>
<Box
sx={{
width: "100%",
bgcolor: "primary.dark",
display: { xs: "none", md: "block" },
}}
>
<Container maxWidth="lg" sx={{ px: 2 }}>
{/* Level 1 Categories Row - Always shown */}
{renderCategoryRow(level1Categories, 1)}
{/* Level 2 Categories Row - Show when level 1 is selected */}
{level2Categories.length > 0 && (
<Box sx={{ mt: 0.5 }}>
{renderCategoryRow(level2Categories, 2)}
</Box>
)}
{/* Level 3 Categories Row - Show when level 2 is selected */}
{level3Categories.length > 0 && (
<Box sx={{ mt: 0.5 }}>
{renderCategoryRow(level3Categories, 3)}
</Box>
)}
</Container>
</Box>
</Profiler>
);
}
}
export default CategoryList;

View File

@@ -0,0 +1,27 @@
import React from "react";
import Box from "@mui/material/Box";
import { Link } from "react-router-dom";
const Logo = () => {
return (
<Box
component={Link}
to="/"
aria-label="Zur Startseite"
sx={{
display: "flex",
alignItems: "center",
textDecoration: "none",
color: "inherit",
}}
>
<img
src="/assets/images/sh.png"
alt="SH Logo"
style={{ height: "45px" }}
/>
</Box>
);
};
export default Logo;

View File

@@ -0,0 +1,310 @@
import React from "react";
import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import Paper from "@mui/material/Paper";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText";
import Typography from "@mui/material/Typography";
import CircularProgress from "@mui/material/CircularProgress";
import SearchIcon from "@mui/icons-material/Search";
import { useNavigate, useLocation } from "react-router-dom";
import SocketContext from "../../contexts/SocketContext.js";
const SearchBar = () => {
const navigate = useNavigate();
const location = useLocation();
const socket = React.useContext(SocketContext);
const searchParams = new URLSearchParams(location.search);
// State management
const [searchQuery, setSearchQuery] = React.useState(
searchParams.get("q") || ""
);
const [suggestions, setSuggestions] = React.useState([]);
const [showSuggestions, setShowSuggestions] = React.useState(false);
const [selectedIndex, setSelectedIndex] = React.useState(-1);
const [loadingSuggestions, setLoadingSuggestions] = React.useState(false);
// Refs for debouncing and timers
const debounceTimerRef = React.useRef(null);
const autocompleteTimerRef = React.useRef(null);
const isFirstKeystrokeRef = React.useRef(true);
const inputRef = React.useRef(null);
const suggestionBoxRef = React.useRef(null);
const handleSearch = (e) => {
e.preventDefault();
delete window.currentSearchQuery;
setShowSuggestions(false);
if (searchQuery.trim()) {
navigate(`/search?q=${encodeURIComponent(searchQuery)}`);
}
};
const updateSearchState = (value) => {
setSearchQuery(value);
// Dispatch global custom event with search query value
const searchEvent = new CustomEvent("search-query-change", {
detail: { query: value },
});
// Store the current search query in the window object
window.currentSearchQuery = value;
window.dispatchEvent(searchEvent);
};
// @note Autocomplete function using getSearchProducts Socket.io API - returns objects with name and seoName
const fetchAutocomplete = React.useCallback(
(query) => {
if (!socket || !query || query.length < 2) {
setSuggestions([]);
setShowSuggestions(false);
setLoadingSuggestions(false);
return;
}
setLoadingSuggestions(true);
socket.emit(
"getSearchProducts",
{
query: query.trim(),
limit: 8,
},
(response) => {
setLoadingSuggestions(false);
if (response && response.products) {
// getSearchProducts returns response.products array
const suggestions = response.products.slice(0, 8); // Limit to 8 suggestions
setSuggestions(suggestions);
setShowSuggestions(suggestions.length > 0);
setSelectedIndex(-1); // Reset selection
} else {
setSuggestions([]);
setShowSuggestions(false);
console.log("getSearchProducts failed or no products:", response);
}
}
);
},
[socket]
);
const handleSearchChange = (e) => {
const value = e.target.value;
// Always update the input field immediately for responsiveness
setSearchQuery(value);
// Clear any existing timers
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
if (autocompleteTimerRef.current) {
clearTimeout(autocompleteTimerRef.current);
}
// Set the debounce timer for search state update
const delay = isFirstKeystrokeRef.current ? 100 : 200;
debounceTimerRef.current = setTimeout(() => {
updateSearchState(value);
isFirstKeystrokeRef.current = false;
// Reset first keystroke flag after 1 second of inactivity
debounceTimerRef.current = setTimeout(() => {
isFirstKeystrokeRef.current = true;
}, 1000);
}, delay);
// Set autocomplete timer with longer delay to reduce API calls
autocompleteTimerRef.current = setTimeout(() => {
fetchAutocomplete(value);
}, 300);
};
// Handle keyboard navigation in suggestions
const handleKeyDown = (e) => {
if (!showSuggestions || suggestions.length === 0) return;
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setSelectedIndex((prev) =>
prev < suggestions.length - 1 ? prev + 1 : prev
);
break;
case "ArrowUp":
e.preventDefault();
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1));
break;
case "Enter":
e.preventDefault();
if (selectedIndex >= 0 && selectedIndex < suggestions.length) {
const selectedSuggestion = suggestions[selectedIndex];
setSearchQuery(selectedSuggestion.name);
updateSearchState(selectedSuggestion.name);
setShowSuggestions(false);
navigate(`/Artikel/${selectedSuggestion.seoName}`);
} else {
handleSearch(e);
}
break;
case "Escape":
setShowSuggestions(false);
setSelectedIndex(-1);
inputRef.current?.blur();
break;
}
};
// Handle suggestion click - navigate to product page directly
const handleSuggestionClick = (suggestion) => {
setSearchQuery(suggestion.name);
updateSearchState(suggestion.name);
setShowSuggestions(false);
navigate(`/Artikel/${suggestion.seoName}`);
};
// Handle input focus
const handleFocus = () => {
if (suggestions.length > 0 && searchQuery.length >= 2) {
setShowSuggestions(true);
}
};
// Handle input blur with delay to allow suggestion clicks
const handleBlur = () => {
setTimeout(() => {
setShowSuggestions(false);
setSelectedIndex(-1);
}, 200);
};
// Clean up timers on unmount
React.useEffect(() => {
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
if (autocompleteTimerRef.current) {
clearTimeout(autocompleteTimerRef.current);
}
};
}, []);
// Close suggestions when clicking outside
React.useEffect(() => {
const handleClickOutside = (event) => {
if (
suggestionBoxRef.current &&
!suggestionBoxRef.current.contains(event.target) &&
!inputRef.current?.contains(event.target)
) {
setShowSuggestions(false);
setSelectedIndex(-1);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<Box
component="form"
onSubmit={handleSearch}
sx={{
flexGrow: 1,
mx: { xs: 1, sm: 2, md: 4 },
position: "relative",
}}
>
<TextField
ref={inputRef}
placeholder="Produkte suchen..."
variant="outlined"
size="small"
fullWidth
value={searchQuery}
onChange={handleSearchChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
endAdornment: loadingSuggestions && (
<InputAdornment position="end">
<CircularProgress size={16} />
</InputAdornment>
),
sx: { borderRadius: 2, bgcolor: "background.paper" },
}}
/>
{/* Autocomplete Suggestions Dropdown */}
{showSuggestions && suggestions.length > 0 && (
<Paper
ref={suggestionBoxRef}
elevation={4}
sx={{
position: "absolute",
top: "100%",
left: 0,
right: 0,
zIndex: 1300,
maxHeight: "300px",
overflow: "auto",
mt: 0.5,
borderRadius: 2,
}}
>
<List disablePadding>
{suggestions.map((suggestion, index) => (
<ListItem
key={suggestion.seoName || index}
button
selected={index === selectedIndex}
onClick={() => handleSuggestionClick(suggestion)}
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: "action.hover",
},
"&.Mui-selected": {
backgroundColor: "action.selected",
"&:hover": {
backgroundColor: "action.selected",
},
},
py: 1,
}}
>
<ListItemText
primary={
<Typography variant="body2" noWrap>
{suggestion.name}
</Typography>
}
/>
</ListItem>
))}
</List>
</Paper>
)}
</Box>
);
};
export default SearchBar;

View File

@@ -0,0 +1,4 @@
export { default as Logo } from './Logo.js';
export { default as SearchBar } from './SearchBar.js';
export { default as ButtonGroupWithRouter } from './ButtonGroup.js';
export { default as CategoryList } from './CategoryList.js';