Refactor App component to replace Home page with MainPageLayout, integrating CarouselProvider for improved page structure. Added new routes for Presseverleih and ThcTest pages, enhancing navigation and organization. Updated Header component to support new page states for Aktionen and Filiale.
This commit is contained in:
BIN
public/assets/images/filiale1.jpg
Normal file
BIN
public/assets/images/filiale1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
BIN
public/assets/images/filiale2.jpg
Normal file
BIN
public/assets/images/filiale2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 242 KiB |
BIN
public/assets/images/presse.jpg
Normal file
BIN
public/assets/images/presse.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
public/assets/images/purpl.jpg
Normal file
BIN
public/assets/images/purpl.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
107
src/App.js
107
src/App.js
@@ -18,13 +18,14 @@ import BugReportIcon from "@mui/icons-material/BugReport";
|
||||
|
||||
import SocketProvider from "./providers/SocketProvider.js";
|
||||
import SocketContext from "./contexts/SocketContext.js";
|
||||
import { CarouselProvider } from "./contexts/CarouselContext.js";
|
||||
import config from "./config.js";
|
||||
import ScrollToTop from "./components/ScrollToTop.js";
|
||||
//import TelemetryService from './services/telemetryService.js';
|
||||
|
||||
import Header from "./components/Header.js";
|
||||
import Footer from "./components/Footer.js";
|
||||
import Home from "./pages/Home.js";
|
||||
import MainPageLayout from "./components/MainPageLayout.js";
|
||||
|
||||
// Lazy load all route components to reduce initial bundle size
|
||||
const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js"));
|
||||
@@ -50,6 +51,10 @@ const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./page
|
||||
const GrowTentKonfigurator = lazy(() => import(/* webpackChunkName: "konfigurator" */ "./pages/GrowTentKonfigurator.js"));
|
||||
const ChatAssistant = lazy(() => import(/* webpackChunkName: "chat" */ "./components/ChatAssistant.js"));
|
||||
|
||||
// Lazy load separate pages that are truly different
|
||||
const PresseverleihPage = lazy(() => import(/* webpackChunkName: "presseverleih" */ "./pages/PresseverleihPage.js"));
|
||||
const ThcTestPage = lazy(() => import(/* webpackChunkName: "thc-test" */ "./pages/ThcTestPage.js"));
|
||||
|
||||
// Import theme from separate file to reduce main bundle size
|
||||
import defaultTheme from "./theme.js";
|
||||
// Lazy load theme customizer for development only
|
||||
@@ -195,60 +200,68 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
||||
<CircularProgress color="primary" />
|
||||
</Box>
|
||||
}>
|
||||
<Routes>
|
||||
{/* Home page with text only */}
|
||||
<Route path="/" element={<Home />} />
|
||||
<CarouselProvider>
|
||||
<Routes>
|
||||
{/* Main pages using unified component */}
|
||||
<Route path="/" element={<MainPageLayout />} />
|
||||
<Route path="/aktionen" element={<MainPageLayout />} />
|
||||
<Route path="/filiale" element={<MainPageLayout />} />
|
||||
|
||||
{/* Category page - Render Content in parallel */}
|
||||
<Route
|
||||
path="/Kategorie/:categoryId"
|
||||
element={<Content socket={socket} socketB={socketB} />}
|
||||
/>
|
||||
{/* Single product page */}
|
||||
<Route
|
||||
path="/Artikel/:seoName"
|
||||
element={<ProductDetailWithSocket />}
|
||||
/>
|
||||
{/* Category page - Render Content in parallel */}
|
||||
<Route
|
||||
path="/Kategorie/:categoryId"
|
||||
element={<Content socket={socket} socketB={socketB} />}
|
||||
/>
|
||||
{/* Single product page */}
|
||||
<Route
|
||||
path="/Artikel/:seoName"
|
||||
element={<ProductDetailWithSocket />}
|
||||
/>
|
||||
|
||||
{/* Search page - Render Content in parallel */}
|
||||
<Route path="/search" element={<Content socket={socket} socketB={socketB} />} />
|
||||
{/* Search page - Render Content in parallel */}
|
||||
<Route path="/search" element={<Content socket={socket} socketB={socketB} />} />
|
||||
|
||||
{/* Profile page */}
|
||||
<Route path="/profile" element={<ProfilePageWithSocket />} />
|
||||
{/* Profile page */}
|
||||
<Route path="/profile" element={<ProfilePageWithSocket />} />
|
||||
|
||||
{/* Reset password page */}
|
||||
<Route
|
||||
path="/resetPassword"
|
||||
element={<ResetPassword socket={socket} socketB={socketB} />}
|
||||
/>
|
||||
{/* Reset password page */}
|
||||
<Route
|
||||
path="/resetPassword"
|
||||
element={<ResetPassword socket={socket} socketB={socketB} />}
|
||||
/>
|
||||
|
||||
{/* Admin page */}
|
||||
<Route path="/admin" element={<AdminPage socket={socket} socketB={socketB} />} />
|
||||
|
||||
{/* Admin Users page */}
|
||||
<Route path="/admin/users" element={<UsersPage socket={socket} socketB={socketB} />} />
|
||||
|
||||
{/* Admin Server Logs page */}
|
||||
<Route path="/admin/logs" element={<ServerLogsPage socket={socket} socketB={socketB} />} />
|
||||
{/* Admin page */}
|
||||
<Route path="/admin" element={<AdminPage socket={socket} socketB={socketB} />} />
|
||||
|
||||
{/* Admin Users page */}
|
||||
<Route path="/admin/users" element={<UsersPage socket={socket} socketB={socketB} />} />
|
||||
|
||||
{/* Admin Server Logs page */}
|
||||
<Route path="/admin/logs" element={<ServerLogsPage socket={socket} socketB={socketB} />} />
|
||||
|
||||
{/* Legal pages */}
|
||||
<Route path="/datenschutz" element={<Datenschutz />} />
|
||||
<Route path="/agb" element={<AGB />} />
|
||||
<Route path="/404" element={<NotFound404 />} />
|
||||
<Route path="/sitemap" element={<Sitemap />} />
|
||||
<Route path="/impressum" element={<Impressum />} />
|
||||
<Route
|
||||
path="/batteriegesetzhinweise"
|
||||
element={<Batteriegesetzhinweise />}
|
||||
/>
|
||||
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
|
||||
{/* Legal pages */}
|
||||
<Route path="/datenschutz" element={<Datenschutz />} />
|
||||
<Route path="/agb" element={<AGB />} />
|
||||
<Route path="/404" element={<NotFound404 />} />
|
||||
<Route path="/sitemap" element={<Sitemap />} />
|
||||
<Route path="/impressum" element={<Impressum />} />
|
||||
<Route
|
||||
path="/batteriegesetzhinweise"
|
||||
element={<Batteriegesetzhinweise />}
|
||||
/>
|
||||
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
|
||||
|
||||
{/* Grow Tent Configurator */}
|
||||
<Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
|
||||
{/* Grow Tent Configurator */}
|
||||
<Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
|
||||
|
||||
{/* Fallback for undefined routes */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
{/* Separate pages that are truly different */}
|
||||
<Route path="/presseverleih" element={<PresseverleihPage />} />
|
||||
<Route path="/thc-test" element={<ThcTestPage />} />
|
||||
|
||||
{/* Fallback for undefined routes */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</CarouselProvider>
|
||||
</Suspense>
|
||||
</Box>
|
||||
{/* Conditionally render the Chat Assistant */}
|
||||
|
||||
@@ -38,7 +38,7 @@ class Header extends Component {
|
||||
render() {
|
||||
// Get socket directly from context in render method
|
||||
const {socket,socketB} = this.context;
|
||||
const { isHomePage, isProfilePage } = this.props;
|
||||
const { isHomePage, isProfilePage, isAktionenPage, isFilialePage } = this.props;
|
||||
|
||||
return (
|
||||
<AppBar position="sticky" color="primary" elevation={0} sx={{ zIndex: 1100 }}>
|
||||
@@ -94,7 +94,7 @@ class Header extends Component {
|
||||
</Box>
|
||||
</Container>
|
||||
</Toolbar>
|
||||
{(isHomePage || this.props.categoryId || isProfilePage) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId} socket={socket} socketB={socketB} />}
|
||||
{(isHomePage || this.props.categoryId || isProfilePage || isAktionenPage || isFilialePage) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId} socket={socket} socketB={socketB} />}
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
@@ -105,10 +105,12 @@ const HeaderWithContext = (props) => {
|
||||
const location = useLocation();
|
||||
const isHomePage = location.pathname === '/';
|
||||
const isProfilePage = location.pathname === '/profile';
|
||||
const isAktionenPage = location.pathname === '/aktionen';
|
||||
const isFilialePage = location.pathname === '/filiale';
|
||||
|
||||
return (
|
||||
<SocketContext.Consumer>
|
||||
{({socket,socketB}) => <Header {...props} socket={socket} socketB={socketB} isHomePage={isHomePage} isProfilePage={isProfilePage} />}
|
||||
{({socket,socketB}) => <Header {...props} socket={socket} socketB={socketB} isHomePage={isHomePage} isProfilePage={isProfilePage} isAktionenPage={isAktionenPage} isFilialePage={isFilialePage} />}
|
||||
</SocketContext.Consumer>
|
||||
);
|
||||
};
|
||||
|
||||
349
src/components/MainPageLayout.js
Normal file
349
src/components/MainPageLayout.js
Normal file
@@ -0,0 +1,349 @@
|
||||
import React from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import Container from "@mui/material/Container";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
||||
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||
import { Link } from "react-router-dom";
|
||||
import SharedCarousel from "./SharedCarousel.js";
|
||||
import { getCombinedAnimatedBorderStyles } from "../utils/animatedBorderStyles.js";
|
||||
|
||||
const MainPageLayout = () => {
|
||||
const location = useLocation();
|
||||
const currentPath = location.pathname;
|
||||
|
||||
// Determine which page we're on
|
||||
const isHome = currentPath === "/";
|
||||
const isAktionen = currentPath === "/aktionen";
|
||||
const isFiliale = currentPath === "/filiale";
|
||||
|
||||
// Get navigation config based on current page
|
||||
const getNavigationConfig = () => {
|
||||
if (isHome) {
|
||||
return {
|
||||
leftNav: { text: "Aktionen", link: "/aktionen" },
|
||||
rightNav: { text: "Filiale", link: "/filiale" }
|
||||
};
|
||||
} else if (isAktionen) {
|
||||
return {
|
||||
leftNav: { text: "Filiale", link: "/filiale" },
|
||||
rightNav: { text: "Home", link: "/" }
|
||||
};
|
||||
} else if (isFiliale) {
|
||||
return {
|
||||
leftNav: { text: "Home", link: "/" },
|
||||
rightNav: { text: "Aktionen", link: "/aktionen" }
|
||||
};
|
||||
}
|
||||
return { leftNav: null, rightNav: null };
|
||||
};
|
||||
|
||||
// Define all titles for layered rendering
|
||||
const allTitles = {
|
||||
home: "ine annabis eeds & uttings",
|
||||
aktionen: "Aktionen",
|
||||
filiale: "Filiale"
|
||||
};
|
||||
|
||||
// Define all content boxes for layered rendering
|
||||
const allContentBoxes = {
|
||||
home: [
|
||||
{
|
||||
title: "Samen",
|
||||
image: "/assets/images/seeds.jpg",
|
||||
bgcolor: "#e1f0d3",
|
||||
link: "/Kategorie/Samen"
|
||||
},
|
||||
{
|
||||
title: "Stecklinge",
|
||||
image: "/assets/images/cutlings.jpg",
|
||||
bgcolor: "#e8f5d6",
|
||||
link: "/Kategorie/Stecklinge"
|
||||
}
|
||||
],
|
||||
aktionen: [
|
||||
{
|
||||
title: "Ölpresse ausleihen",
|
||||
image: "/assets/images/presse.jpg",
|
||||
bgcolor: "#e1f0d3",
|
||||
link: "/presseverleih"
|
||||
},
|
||||
{
|
||||
title: "THC Test",
|
||||
image: "/assets/images/purpl.jpg",
|
||||
bgcolor: "#e8f5d6",
|
||||
link: "/thc-test"
|
||||
}
|
||||
],
|
||||
filiale: [
|
||||
{
|
||||
title: "Trachenberger Straße 14",
|
||||
image: "/assets/images/filiale1.jpg",
|
||||
bgcolor: "#e1f0d3",
|
||||
link: "/filiale"
|
||||
},
|
||||
{
|
||||
title: "01129 Dresden",
|
||||
image: "/assets/images/filiale2.jpg",
|
||||
bgcolor: "#e8f5d6",
|
||||
link: "/filiale"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Get opacity for each page layer
|
||||
const getOpacity = (pageType) => {
|
||||
if (pageType === "home" && isHome) return 1;
|
||||
if (pageType === "aktionen" && isAktionen) return 1;
|
||||
if (pageType === "filiale" && isFiliale) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
const navConfig = getNavigationConfig();
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ py: 2 }}>
|
||||
<style>{getCombinedAnimatedBorderStyles(['seeds', 'cutlings'])}</style>
|
||||
|
||||
{/* Main Navigation Header */}
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
mb: 4,
|
||||
mt: 2,
|
||||
px: 0,
|
||||
transition: "all 0.3s ease-in-out"
|
||||
}}>
|
||||
{/* Left Navigation - Layered rendering */}
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
justifyContent: "flex-start",
|
||||
position: "relative",
|
||||
mr: 2
|
||||
}}>
|
||||
{["Aktionen", "Filiale", "Home"].map((text, index) => {
|
||||
const isActive = navConfig.leftNav && navConfig.leftNav.text === text;
|
||||
const link = text === "Aktionen" ? "/aktionen" : text === "Filiale" ? "/filiale" : "/";
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={text}
|
||||
component={Link}
|
||||
to={link}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
transition: "all 0.3s ease",
|
||||
opacity: isActive ? 1 : 0,
|
||||
position: index === 0 ? "relative" : "absolute",
|
||||
left: index !== 0 ? 0 : "auto",
|
||||
pointerEvents: isActive ? "auto" : "none",
|
||||
"&:hover": {
|
||||
transform: "translateX(-5px)",
|
||||
color: "primary.main"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChevronLeft sx={{ fontSize: "2rem", mr: 1 }} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: "SwashingtonCP",
|
||||
fontSize: { xs: "1.25rem", sm: "1.25rem", md: "2.125rem" },
|
||||
textShadow: "2px 2px 4px rgba(0, 0, 0, 0.3)",
|
||||
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* Center Title - Layered rendering - This defines the height for centering */}
|
||||
<Box sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
px: 0,
|
||||
position: "relative",
|
||||
minWidth: 0
|
||||
}}>
|
||||
{Object.entries(allTitles).map(([pageType, title]) => (
|
||||
<Typography
|
||||
key={pageType}
|
||||
variant="h3"
|
||||
component="h1"
|
||||
sx={{
|
||||
fontFamily: "SwashingtonCP",
|
||||
fontSize: { xs: "2.125rem", sm: "2.125rem", md: "3rem" },
|
||||
textAlign: "center",
|
||||
color: "primary.main",
|
||||
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)",
|
||||
transition: "opacity 0.5s ease-in-out",
|
||||
opacity: getOpacity(pageType),
|
||||
position: pageType === "home" ? "relative" : "absolute",
|
||||
top: pageType !== "home" ? "50%" : "auto",
|
||||
left: pageType !== "home" ? "50%" : "auto",
|
||||
transform: pageType !== "home" ? "translate(-50%, -50%)" : "none",
|
||||
width: "100%",
|
||||
pointerEvents: getOpacity(pageType) === 1 ? "auto" : "none",
|
||||
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
|
||||
wordWrap: "break-word",
|
||||
hyphens: "auto"
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Right Navigation - Layered rendering */}
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
justifyContent: "flex-end",
|
||||
position: "relative",
|
||||
ml: 2
|
||||
}}>
|
||||
{["Aktionen", "Filiale", "Home"].map((text, index) => {
|
||||
const isActive = navConfig.rightNav && navConfig.rightNav.text === text;
|
||||
const link = text === "Aktionen" ? "/aktionen" : text === "Filiale" ? "/filiale" : "/";
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={text}
|
||||
component={Link}
|
||||
to={link}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
transition: "all 0.3s ease",
|
||||
opacity: isActive ? 1 : 0,
|
||||
position: index === 0 ? "relative" : "absolute",
|
||||
right: index !== 0 ? 0 : "auto",
|
||||
pointerEvents: isActive ? "auto" : "none",
|
||||
"&:hover": {
|
||||
transform: "translateX(5px)",
|
||||
color: "primary.main"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: "SwashingtonCP",
|
||||
fontSize: { xs: "1.25rem", sm: "1.25rem", md: "2.125rem" },
|
||||
textShadow: "2px 2px 4px rgba(0, 0, 0, 0.3)",
|
||||
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
<ChevronRight sx={{ fontSize: "2rem", ml: 1 }} />
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Content Boxes - Layered rendering */}
|
||||
<Box sx={{ position: "relative", mb: 4 }}>
|
||||
{Object.entries(allContentBoxes).map(([pageType, contentBoxes]) => (
|
||||
<Grid
|
||||
key={pageType}
|
||||
container
|
||||
spacing={0}
|
||||
sx={{
|
||||
transition: "opacity 0.5s ease-in-out",
|
||||
opacity: getOpacity(pageType),
|
||||
position: pageType === "home" ? "relative" : "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
pointerEvents: getOpacity(pageType) === 1 ? "auto" : "none"
|
||||
}}
|
||||
>
|
||||
{contentBoxes.map((box, index) => (
|
||||
<Grid key={`${pageType}-${index}`} item xs={12} sm={6} sx={{ p: 2, width: "50%" }}>
|
||||
<div className={`animated-border-card ${index === 0 ? 'seeds-card' : 'cutlings-card'}`}>
|
||||
<Paper
|
||||
component={Link}
|
||||
to={box.link}
|
||||
sx={{
|
||||
p: 0,
|
||||
textDecoration: "none",
|
||||
color: "text.primary",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
height: { xs: 150, sm: 200, md: 300 },
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: 10,
|
||||
transition: "all 0.3s ease",
|
||||
"&:hover": {
|
||||
transform: "translateY(-5px)",
|
||||
boxShadow: 20,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
bgcolor: box.bgcolor,
|
||||
backgroundImage: `url("${box.image}")`,
|
||||
backgroundSize: "contain",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bgcolor: "rgba(27, 94, 32, 0.8)",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1.6rem",
|
||||
color: "white",
|
||||
fontFamily: "SwashingtonCP",
|
||||
}}
|
||||
>
|
||||
{box.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</div>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Shared Carousel */}
|
||||
<SharedCarousel />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPageLayout;
|
||||
229
src/components/SharedCarousel.js
Normal file
229
src/components/SharedCarousel.js
Normal file
@@ -0,0 +1,229 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
||||
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||
import CategoryBox from "./CategoryBox.js";
|
||||
import SocketContext from "../contexts/SocketContext.js";
|
||||
import { useCarousel } from "../contexts/CarouselContext.js";
|
||||
|
||||
// Helper to process and set categories
|
||||
const processCategoryTree = (categoryTree) => {
|
||||
if (
|
||||
categoryTree &&
|
||||
categoryTree.id === 209 &&
|
||||
Array.isArray(categoryTree.children)
|
||||
) {
|
||||
return categoryTree.children;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Check for cached data
|
||||
const getProductCache = () => {
|
||||
if (typeof window !== "undefined" && window.productCache) {
|
||||
return window.productCache;
|
||||
}
|
||||
if (
|
||||
typeof global !== "undefined" &&
|
||||
global.window &&
|
||||
global.window.productCache
|
||||
) {
|
||||
return global.window.productCache;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Initialize categories
|
||||
const initializeCategories = () => {
|
||||
const productCache = getProductCache();
|
||||
|
||||
if (productCache && productCache["categoryTree_209"]) {
|
||||
const cached = productCache["categoryTree_209"];
|
||||
if (cached.categoryTree) {
|
||||
return processCategoryTree(cached.categoryTree);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const SharedCarousel = () => {
|
||||
const { carouselRef, filteredCategories, setFilteredCategories, moveCarousel } = useCarousel();
|
||||
const context = useContext(SocketContext);
|
||||
const [rootCategories, setRootCategories] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const initialCategories = initializeCategories();
|
||||
setRootCategories(initialCategories);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch from socket if we don't already have categories
|
||||
if (
|
||||
rootCategories.length === 0 &&
|
||||
context && context.socket && context.socket.connected &&
|
||||
typeof window !== "undefined"
|
||||
) {
|
||||
context.socket.emit("categoryList", { categoryId: 209 }, (response) => {
|
||||
if (response && response.categoryTree) {
|
||||
// Store in cache
|
||||
try {
|
||||
if (!window.productCache) window.productCache = {};
|
||||
window.productCache["categoryTree_209"] = {
|
||||
categoryTree: response.categoryTree,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
setRootCategories(response.categoryTree.children || []);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [context, context?.socket?.connected, rootCategories.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const filtered = rootCategories.filter(
|
||||
(cat) => cat.id !== 689 && cat.id !== 706
|
||||
);
|
||||
setFilteredCategories(filtered);
|
||||
}, [rootCategories, setFilteredCategories]);
|
||||
|
||||
// Create duplicated array for seamless scrolling
|
||||
const displayCategories = [...filteredCategories, ...filteredCategories];
|
||||
|
||||
if (filteredCategories.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
mb: 2,
|
||||
fontFamily: "SwashingtonCP",
|
||||
color: "primary.main",
|
||||
textAlign: "center",
|
||||
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
|
||||
}}
|
||||
>
|
||||
Kategorien
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
className="carousel-wrapper"
|
||||
style={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
padding: '0 20px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
>
|
||||
{/* Left Arrow */}
|
||||
<IconButton
|
||||
onClick={() => moveCarousel("left")}
|
||||
aria-label="Vorherige Kategorien anzeigen"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '8px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1200,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '50%'
|
||||
}}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</IconButton>
|
||||
|
||||
{/* Right Arrow */}
|
||||
<IconButton
|
||||
onClick={() => moveCarousel("right")}
|
||||
aria-label="Nächste Kategorien anzeigen"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
right: '8px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1200,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '50%'
|
||||
}}
|
||||
>
|
||||
<ChevronRight />
|
||||
</IconButton>
|
||||
|
||||
<div
|
||||
className="carousel-container"
|
||||
style={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
padding: '20px 0',
|
||||
width: '100%',
|
||||
maxWidth: '1080px',
|
||||
margin: '0 auto',
|
||||
zIndex: 1,
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="home-carousel-track"
|
||||
ref={carouselRef}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
transition: 'none',
|
||||
alignItems: 'flex-start',
|
||||
width: 'fit-content',
|
||||
overflow: 'visible',
|
||||
position: 'relative',
|
||||
transform: 'translateX(0px)',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
>
|
||||
{displayCategories.map((category, index) => (
|
||||
<div
|
||||
key={`${category.id}-${index}`}
|
||||
className="carousel-item"
|
||||
style={{
|
||||
flex: '0 0 130px',
|
||||
width: '130px',
|
||||
maxWidth: '130px',
|
||||
minWidth: '130px',
|
||||
height: '130px',
|
||||
maxHeight: '130px',
|
||||
minHeight: '130px',
|
||||
boxSizing: 'border-box',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<CategoryBox
|
||||
id={category.id}
|
||||
name={category.name}
|
||||
seoName={category.seoName}
|
||||
image={category.image}
|
||||
bgcolor={category.bgcolor}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SharedCarousel;
|
||||
225
src/contexts/CarouselContext.js
Normal file
225
src/contexts/CarouselContext.js
Normal file
@@ -0,0 +1,225 @@
|
||||
import React, { createContext, useContext, useRef, useEffect, useState } from 'react';
|
||||
|
||||
const CarouselContext = createContext();
|
||||
|
||||
export const useCarousel = () => {
|
||||
const context = useContext(CarouselContext);
|
||||
if (!context) {
|
||||
throw new Error('useCarousel must be used within a CarouselProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const CarouselProvider = ({ children }) => {
|
||||
const carouselRef = useRef(null);
|
||||
const scrollPositionRef = useRef(0);
|
||||
const animationIdRef = useRef(null);
|
||||
const isPausedRef = useRef(false);
|
||||
const resumeTimeoutRef = useRef(null);
|
||||
|
||||
const [filteredCategories, setFilteredCategories] = useState([]);
|
||||
|
||||
// Initialize refs properly
|
||||
useEffect(() => {
|
||||
isPausedRef.current = false;
|
||||
scrollPositionRef.current = 0;
|
||||
}, []);
|
||||
|
||||
// Auto-scroll effect
|
||||
useEffect(() => {
|
||||
if (filteredCategories.length === 0) return;
|
||||
|
||||
const startAnimation = () => {
|
||||
if (!carouselRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isPausedRef.current = false;
|
||||
|
||||
const itemWidth = 146; // 130px + 16px gap
|
||||
const totalWidth = filteredCategories.length * itemWidth;
|
||||
|
||||
const animate = () => {
|
||||
if (!animationIdRef.current || isPausedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollPositionRef.current += 0.5;
|
||||
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = 0;
|
||||
}
|
||||
|
||||
if (carouselRef.current) {
|
||||
const transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
carouselRef.current.style.transform = transform;
|
||||
}
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
if (!isPausedRef.current) {
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Try immediately, then with increasing delays
|
||||
if (!startAnimation()) {
|
||||
const timeout1 = setTimeout(() => {
|
||||
if (!startAnimation()) {
|
||||
const timeout2 = setTimeout(() => {
|
||||
if (!startAnimation()) {
|
||||
const timeout3 = setTimeout(startAnimation, 2000);
|
||||
return () => clearTimeout(timeout3);
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearTimeout(timeout2);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
isPausedRef.current = true;
|
||||
clearTimeout(timeout1);
|
||||
if (animationIdRef.current) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
}
|
||||
if (resumeTimeoutRef.current) {
|
||||
clearTimeout(resumeTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
isPausedRef.current = true;
|
||||
if (animationIdRef.current) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
}
|
||||
if (resumeTimeoutRef.current) {
|
||||
clearTimeout(resumeTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [filteredCategories]);
|
||||
|
||||
// Additional effect for when ref becomes available
|
||||
useEffect(() => {
|
||||
if (filteredCategories.length > 0 && carouselRef.current && !animationIdRef.current) {
|
||||
isPausedRef.current = false;
|
||||
|
||||
const itemWidth = 146;
|
||||
const totalWidth = filteredCategories.length * itemWidth;
|
||||
|
||||
const animate = () => {
|
||||
if (!animationIdRef.current || isPausedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollPositionRef.current += 0.5;
|
||||
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = 0;
|
||||
}
|
||||
|
||||
if (carouselRef.current) {
|
||||
const transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
carouselRef.current.style.transform = transform;
|
||||
}
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
if (!isPausedRef.current) {
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Manual navigation
|
||||
const moveCarousel = (direction) => {
|
||||
if (!carouselRef.current) return;
|
||||
|
||||
// Pause auto-scroll
|
||||
isPausedRef.current = true;
|
||||
if (animationIdRef.current) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
animationIdRef.current = null;
|
||||
}
|
||||
|
||||
const itemWidth = 146;
|
||||
const moveAmount = itemWidth * 3;
|
||||
const totalWidth = filteredCategories.length * itemWidth;
|
||||
|
||||
if (direction === "left") {
|
||||
scrollPositionRef.current -= moveAmount;
|
||||
if (scrollPositionRef.current < 0) {
|
||||
scrollPositionRef.current = totalWidth + scrollPositionRef.current;
|
||||
}
|
||||
} else {
|
||||
scrollPositionRef.current += moveAmount;
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = scrollPositionRef.current % totalWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply smooth transition
|
||||
carouselRef.current.style.transition = "transform 0.5s ease-in-out";
|
||||
carouselRef.current.style.transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
|
||||
// Remove transition after animation
|
||||
setTimeout(() => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.style.transition = "none";
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Clear existing timeout
|
||||
if (resumeTimeoutRef.current) {
|
||||
clearTimeout(resumeTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Resume auto-scroll after 3 seconds
|
||||
resumeTimeoutRef.current = setTimeout(() => {
|
||||
isPausedRef.current = false;
|
||||
|
||||
const animate = () => {
|
||||
if (!animationIdRef.current || isPausedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollPositionRef.current += 0.5;
|
||||
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = 0;
|
||||
}
|
||||
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.style.transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
}
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const value = {
|
||||
carouselRef,
|
||||
scrollPositionRef,
|
||||
animationIdRef,
|
||||
isPausedRef,
|
||||
resumeTimeoutRef,
|
||||
filteredCategories,
|
||||
setFilteredCategories,
|
||||
moveCarousel
|
||||
};
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider value={value}>
|
||||
{children}
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CarouselContext;
|
||||
@@ -1,652 +0,0 @@
|
||||
import React, { useState, useEffect, useContext, useRef } from "react";
|
||||
import Container from "@mui/material/Container";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
||||
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||
import { Link } from "react-router-dom";
|
||||
import CategoryBox from "../components/CategoryBox.js";
|
||||
import SocketContext from "../contexts/SocketContext.js";
|
||||
import { getCombinedAnimatedBorderStyles } from "../utils/animatedBorderStyles.js";
|
||||
|
||||
// @note SwashingtonCP font is now loaded globally via index.css
|
||||
|
||||
// Carousel styles - Simple styles for JavaScript-based animation
|
||||
const carouselStyles = `
|
||||
.carousel-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
.carousel-wrapper .carousel-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 20px 0;
|
||||
width: 100%;
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.carousel-wrapper .home-carousel-track {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
transition: none;
|
||||
align-items: flex-start;
|
||||
width: 1200px;
|
||||
max-width: 100%;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.carousel-wrapper .carousel-item {
|
||||
flex: 0 0 130px;
|
||||
width: 130px !important;
|
||||
max-width: 130px;
|
||||
min-width: 130px;
|
||||
height: 130px !important;
|
||||
max-height: 130px;
|
||||
min-height: 130px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.carousel-nav-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 20;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.carousel-nav-button:hover {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.carousel-nav-left {
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.carousel-nav-right {
|
||||
right: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
// Generate combined styles for both seeds and cutlings cards
|
||||
const animatedBorderStyle = getCombinedAnimatedBorderStyles([
|
||||
"seeds",
|
||||
"cutlings",
|
||||
]);
|
||||
|
||||
const Home = () => {
|
||||
const carouselRef = useRef(null);
|
||||
const scrollPositionRef = useRef(0);
|
||||
const animationIdRef = useRef(null);
|
||||
const isPausedRef = useRef(false);
|
||||
const resumeTimeoutRef = useRef(null);
|
||||
|
||||
// @note Initialize refs properly
|
||||
useEffect(() => {
|
||||
isPausedRef.current = false;
|
||||
scrollPositionRef.current = 0;
|
||||
}, []);
|
||||
// Helper to process and set categories
|
||||
const processCategoryTree = (categoryTree) => {
|
||||
if (
|
||||
categoryTree &&
|
||||
categoryTree.id === 209 &&
|
||||
Array.isArray(categoryTree.children)
|
||||
) {
|
||||
return categoryTree.children;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Check for cached data - handle both browser and prerender environments
|
||||
const getProductCache = () => {
|
||||
if (typeof window !== "undefined" && window.productCache) {
|
||||
return window.productCache;
|
||||
}
|
||||
if (
|
||||
typeof global !== "undefined" &&
|
||||
global.window &&
|
||||
global.window.productCache
|
||||
) {
|
||||
return global.window.productCache;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Initialize rootCategories from cache if available (for prerendering)
|
||||
const initializeCategories = () => {
|
||||
const productCache = getProductCache();
|
||||
|
||||
if (productCache && productCache["categoryTree_209"]) {
|
||||
const cached = productCache["categoryTree_209"];
|
||||
//const cacheAge = Date.now() - cached.timestamp;
|
||||
//const tenMinutes = 10 * 60 * 1000;
|
||||
if (/*cacheAge < tenMinutes &&*/ cached.categoryTree) {
|
||||
return processCategoryTree(cached.categoryTree);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const [rootCategories, setRootCategories] = useState(() =>
|
||||
initializeCategories()
|
||||
);
|
||||
const context = useContext(SocketContext);
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch from socket if we don't already have categories and we're in browser
|
||||
if (
|
||||
rootCategories.length === 0 &&
|
||||
context && context.socket && context.socket.connected &&
|
||||
typeof window !== "undefined"
|
||||
) {
|
||||
context.socket.emit("categoryList", { categoryId: 209 }, (response) => {
|
||||
if (response && response.categoryTree) {
|
||||
// Store in cache
|
||||
try {
|
||||
if (!window.productCache) window.productCache = {};
|
||||
window.productCache["categoryTree_209"] = {
|
||||
categoryTree: response.categoryTree,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
setRootCategories(response.categoryTree.children || []);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [context, context?.socket?.connected, rootCategories.length]);
|
||||
|
||||
// Filter categories (excluding specific IDs)
|
||||
const filteredCategories = rootCategories.filter(
|
||||
(cat) => cat.id !== 689 && cat.id !== 706
|
||||
);
|
||||
|
||||
// Create duplicated array for seamless scrolling
|
||||
const displayCategories = [...filteredCategories, ...filteredCategories];
|
||||
|
||||
// Auto-scroll effect
|
||||
useEffect(() => {
|
||||
if (filteredCategories.length === 0) return;
|
||||
|
||||
// @note Add a small delay to ensure DOM is ready after prerender
|
||||
const startAnimation = () => {
|
||||
if (!carouselRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @note Reset paused state when starting animation
|
||||
isPausedRef.current = false;
|
||||
|
||||
const itemWidth = 146; // 130px + 16px gap
|
||||
const totalWidth = filteredCategories.length * itemWidth;
|
||||
|
||||
const animate = () => {
|
||||
// Check if we should be animating
|
||||
if (!animationIdRef.current || isPausedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollPositionRef.current += 0.5; // Speed of scrolling
|
||||
|
||||
// Reset position for seamless loop
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = 0;
|
||||
}
|
||||
|
||||
if (carouselRef.current) {
|
||||
const transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
carouselRef.current.style.transform = transform;
|
||||
}
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
// Only start animation if not paused
|
||||
if (!isPausedRef.current) {
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Try immediately, then with increasing delays to handle prerender scenarios
|
||||
if (!startAnimation()) {
|
||||
const timeout1 = setTimeout(() => {
|
||||
if (!startAnimation()) {
|
||||
const timeout2 = setTimeout(() => {
|
||||
if (!startAnimation()) {
|
||||
const timeout3 = setTimeout(startAnimation, 2000);
|
||||
return () => clearTimeout(timeout3);
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearTimeout(timeout2);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
isPausedRef.current = true;
|
||||
clearTimeout(timeout1);
|
||||
if (animationIdRef.current) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
}
|
||||
if (resumeTimeoutRef.current) {
|
||||
clearTimeout(resumeTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
isPausedRef.current = true;
|
||||
if (animationIdRef.current) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
}
|
||||
if (resumeTimeoutRef.current) {
|
||||
clearTimeout(resumeTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [filteredCategories]);
|
||||
|
||||
// Additional effect to handle cases where categories are available but ref wasn't ready
|
||||
useEffect(() => {
|
||||
if (filteredCategories.length > 0 && carouselRef.current && !animationIdRef.current) {
|
||||
// @note Reset paused state when starting animation
|
||||
isPausedRef.current = false;
|
||||
|
||||
const itemWidth = 146;
|
||||
const totalWidth = filteredCategories.length * itemWidth;
|
||||
|
||||
const animate = () => {
|
||||
if (!animationIdRef.current || isPausedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollPositionRef.current += 0.5;
|
||||
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = 0;
|
||||
}
|
||||
|
||||
if (carouselRef.current) {
|
||||
const transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
carouselRef.current.style.transform = transform;
|
||||
}
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
if (!isPausedRef.current) {
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Manual navigation
|
||||
const moveCarousel = (direction) => {
|
||||
if (!carouselRef.current) return;
|
||||
|
||||
// Pause auto-scroll
|
||||
isPausedRef.current = true;
|
||||
if (animationIdRef.current) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
animationIdRef.current = null;
|
||||
}
|
||||
|
||||
const itemWidth = 146;
|
||||
const moveAmount = itemWidth * 3; // Move 3 items at a time
|
||||
const totalWidth = filteredCategories.length * itemWidth;
|
||||
|
||||
if (direction === "left") {
|
||||
scrollPositionRef.current -= moveAmount;
|
||||
// Handle wrapping for infinite scroll
|
||||
if (scrollPositionRef.current < 0) {
|
||||
scrollPositionRef.current = totalWidth + scrollPositionRef.current;
|
||||
}
|
||||
} else {
|
||||
scrollPositionRef.current += moveAmount;
|
||||
// Handle wrapping for infinite scroll
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = scrollPositionRef.current % totalWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply smooth transition for manual navigation
|
||||
carouselRef.current.style.transition = "transform 0.5s ease-in-out";
|
||||
carouselRef.current.style.transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
|
||||
// Remove transition after animation completes
|
||||
setTimeout(() => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.style.transition = "none";
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Clear any existing resume timeout
|
||||
if (resumeTimeoutRef.current) {
|
||||
clearTimeout(resumeTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Resume auto-scroll after 3 seconds
|
||||
resumeTimeoutRef.current = setTimeout(() => {
|
||||
isPausedRef.current = false;
|
||||
|
||||
const animate = () => {
|
||||
if (!animationIdRef.current || isPausedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollPositionRef.current += 0.5;
|
||||
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = 0;
|
||||
}
|
||||
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.style.transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
}
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ pt: 4, pb: 2, maxWidth: '1200px !important' }}>
|
||||
{/* Inject the animated border and carousel styles */}
|
||||
<style>{animatedBorderStyle}</style>
|
||||
<style>{carouselStyles}</style>
|
||||
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="h1"
|
||||
sx={{
|
||||
mb: 4,
|
||||
fontFamily: "SwashingtonCP",
|
||||
color: "primary.main",
|
||||
textAlign: "center",
|
||||
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
|
||||
}}
|
||||
>
|
||||
ine annabis eeds & uttings
|
||||
</Typography>
|
||||
|
||||
<Grid container sx={{ display: "flex", flexDirection: "row" }}>
|
||||
{/* Seeds Category Box */}
|
||||
<Grid item xs={12} sm={6} sx={{ p: 2, width: "50%" }}>
|
||||
<div className="animated-border-card seeds-card">
|
||||
<Paper
|
||||
component={Link}
|
||||
to="/Kategorie/Seeds"
|
||||
sx={{
|
||||
p: 0,
|
||||
textDecoration: "none",
|
||||
color: "text.primary",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
height: { xs: 150, sm: 200, md: 300 },
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
transition: "all 0.3s ease",
|
||||
boxShadow: 10,
|
||||
"&:hover": {
|
||||
transform: "translateY(-5px)",
|
||||
boxShadow: 20,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Image Container - Place your seeds image here */}
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
bgcolor: "#e1f0d3",
|
||||
backgroundImage: 'url("/assets/images/seeds.jpg")',
|
||||
backgroundSize: "contain",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Overlay text - optional */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bgcolor: "rgba(27, 94, 32, 0.8)",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1.6rem",
|
||||
color: "white",
|
||||
fontFamily: "SwashingtonCP",
|
||||
}}
|
||||
>
|
||||
Seeds
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
{/* Cutlings Category Box */}
|
||||
<Grid item xs={12} sm={6} sx={{ p: 2, width: "50%" }}>
|
||||
<div className="animated-border-card cutlings-card">
|
||||
<Paper
|
||||
component={Link}
|
||||
to="/Kategorie/Stecklinge"
|
||||
sx={{
|
||||
p: 0,
|
||||
textDecoration: "none",
|
||||
color: "text.primary",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
height: { xs: 150, sm: 200, md: 300 },
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: 10,
|
||||
transition: "all 0.3s ease",
|
||||
"&:hover": {
|
||||
transform: "translateY(-5px)",
|
||||
boxShadow: 20,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Image Container - Place your cutlings image here */}
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
bgcolor: "#e8f5d6",
|
||||
backgroundImage: 'url("/assets/images/cutlings.jpg")',
|
||||
backgroundSize: "contain",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Overlay text - optional */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bgcolor: "rgba(27, 94, 32, 0.8)",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1.6rem",
|
||||
color: "white",
|
||||
fontFamily: "SwashingtonCP",
|
||||
}}
|
||||
>
|
||||
Stecklinge
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Continuous Rotating Carousel for Categories */}
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
mb: 2,
|
||||
fontFamily: "SwashingtonCP",
|
||||
color: "primary.main",
|
||||
textAlign: "center",
|
||||
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
|
||||
}}
|
||||
>
|
||||
Kategorien
|
||||
</Typography>
|
||||
|
||||
{filteredCategories.length > 0 && (
|
||||
<div
|
||||
className="carousel-wrapper"
|
||||
style={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
padding: '0 20px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
>
|
||||
{/* Left Arrow */}
|
||||
<IconButton
|
||||
onClick={() => moveCarousel("left")}
|
||||
aria-label="Vorherige Kategorien anzeigen"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '8px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1200,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '50%'
|
||||
}}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</IconButton>
|
||||
|
||||
{/* Right Arrow */}
|
||||
<IconButton
|
||||
onClick={() => moveCarousel("right")}
|
||||
aria-label="Nächste Kategorien anzeigen"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
right: '8px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1200,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '50%'
|
||||
}}
|
||||
>
|
||||
<ChevronRight />
|
||||
</IconButton>
|
||||
|
||||
<div
|
||||
className="carousel-container"
|
||||
style={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
padding: '20px 0',
|
||||
width: '100%',
|
||||
maxWidth: '1080px',
|
||||
margin: '0 auto',
|
||||
zIndex: 1,
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="home-carousel-track"
|
||||
ref={carouselRef}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
transition: 'none',
|
||||
alignItems: 'flex-start',
|
||||
width: 'fit-content',
|
||||
overflow: 'visible',
|
||||
position: 'relative',
|
||||
transform: 'translateX(0px)',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
>
|
||||
{displayCategories.map((category, index) => (
|
||||
<div
|
||||
key={`${category.id}-${index}`}
|
||||
className="carousel-item"
|
||||
style={{
|
||||
flex: '0 0 130px',
|
||||
width: '130px',
|
||||
maxWidth: '130px',
|
||||
minWidth: '130px',
|
||||
height: '130px',
|
||||
maxHeight: '130px',
|
||||
minHeight: '130px',
|
||||
boxSizing: 'border-box',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<CategoryBox
|
||||
id={category.id}
|
||||
name={category.name}
|
||||
seoName={category.seoName}
|
||||
image={category.image}
|
||||
bgcolor={category.bgcolor}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
46
src/pages/PresseverleihPage.js
Normal file
46
src/pages/PresseverleihPage.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import Container from "@mui/material/Container";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
const PresseverleihPage = () => {
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ pt: 4, pb: 2, maxWidth: '1200px !important' }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="h1"
|
||||
sx={{
|
||||
mb: 4,
|
||||
fontFamily: "SwashingtonCP",
|
||||
color: "primary.main",
|
||||
textAlign: "center",
|
||||
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
|
||||
}}
|
||||
>
|
||||
Ölpresse ausleihen
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "50vh",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
fontStyle: "italic"
|
||||
}}
|
||||
>
|
||||
Inhalt kommt bald...
|
||||
</Typography>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default PresseverleihPage;
|
||||
46
src/pages/ThcTestPage.js
Normal file
46
src/pages/ThcTestPage.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import Container from "@mui/material/Container";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
const ThcTestPage = () => {
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ pt: 4, pb: 2, maxWidth: '1200px !important' }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="h1"
|
||||
sx={{
|
||||
mb: 4,
|
||||
fontFamily: "SwashingtonCP",
|
||||
color: "primary.main",
|
||||
textAlign: "center",
|
||||
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
|
||||
}}
|
||||
>
|
||||
THC Test
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "50vh",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
fontStyle: "italic"
|
||||
}}
|
||||
>
|
||||
Inhalt kommt bald...
|
||||
</Typography>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThcTestPage;
|
||||
Reference in New Issue
Block a user