feat: enhance image loading and socket handling in Product and Images components, and update prerender logic in App and ProductDetailPage

This commit is contained in:
sebseb7
2025-07-20 11:53:27 +02:00
parent 92987a518b
commit 3d136775e2
9 changed files with 188 additions and 97 deletions

View File

@@ -57,7 +57,7 @@ const config = require("./prerender/config.cjs");
const shopConfig = require("./src/config.js").default; const shopConfig = require("./src/config.js").default;
const { renderPage } = require("./prerender/renderer.cjs"); const { renderPage } = require("./prerender/renderer.cjs");
const { generateProductMetaTags, generateProductJsonLd } = require("./prerender/seo.cjs"); const { generateProductMetaTags, generateProductJsonLd } = require("./prerender/seo.cjs");
const { fetchProductDetails } = require("./prerender/data-fetching.cjs"); const { fetchProductDetails, saveProductImages } = require("./prerender/data-fetching.cjs");
// Import product component // Import product component
const PrerenderProduct = require("./src/PrerenderProduct.js").default; const PrerenderProduct = require("./src/PrerenderProduct.js").default;

View File

@@ -178,11 +178,16 @@ const renderPage = (
const prerenderFallbackScript = ` const prerenderFallbackScript = `
<script> <script>
// Save prerendered content to window object for SocketProvider fallback // Save prerendered content to window object for SocketProvider fallback
window.__PRERENDER_FALLBACK__ = { window.__PRERENDER_FALLBACK__ = {
path: '${location}', path: '${location}',
content: ${JSON.stringify(renderedMarkup)}, content: ${JSON.stringify(renderedMarkup)},
timestamp: ${Date.now()} timestamp: ${Date.now()}
}; };
// DEBUG: Multiple alerts throughout the loading process
// Debug alerts removed
</script> </script>
`; `;
@@ -239,8 +244,10 @@ const renderPage = (
let newHtml; let newHtml;
if (rootDivRegex.test(template)) { if (rootDivRegex.test(template)) {
if (!suppressLogs) console.log(` 📝 Root div found, replacing with ${renderedMarkup.length} chars of markup`);
newHtml = template.replace(rootDivRegex, replacementHtml); newHtml = template.replace(rootDivRegex, replacementHtml);
} else { } else {
if (!suppressLogs) console.log(` ⚠️ No root div found, appending to body`);
newHtml = template.replace("<body>", `<body>${replacementHtml}`); newHtml = template.replace("<body>", `<body>${replacementHtml}`);
} }

View File

@@ -32,11 +32,15 @@ import Header from "./components/Header.js";
import Footer from "./components/Footer.js"; import Footer from "./components/Footer.js";
import MainPageLayout from "./components/MainPageLayout.js"; import MainPageLayout from "./components/MainPageLayout.js";
// Lazy load all route components to reduce initial bundle size // TEMPORARILY DISABLE ALL LAZY LOADING TO ELIMINATE CircularProgress
const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js")); import Content from "./components/Content.js";
const ProductDetailWithSocket = lazy(() => import(/* webpackChunkName: "product-detail" */ "./components/ProductDetailWithSocket.js")); import ProductDetailWithSocket from "./components/ProductDetailWithSocket.js";
const ProfilePageWithSocket = lazy(() => import(/* webpackChunkName: "profile" */ "./pages/ProfilePage.js")); import ProfilePageWithSocket from "./pages/ProfilePage.js";
const ResetPassword = lazy(() => import(/* webpackChunkName: "reset-password" */ "./pages/ResetPassword.js")); import ResetPassword from "./pages/ResetPassword.js";
// const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js"));
// const ProductDetailWithSocket = lazy(() => import(/* webpackChunkName: "product-detail" */ "./components/ProductDetailWithSocket.js"));
// const ProfilePageWithSocket = lazy(() => import(/* webpackChunkName: "profile" */ "./pages/ProfilePage.js"));
// const ResetPassword = lazy(() => import(/* webpackChunkName: "reset-password" */ "./pages/ResetPassword.js"));
// Lazy load admin pages - only loaded when admin users access them // Lazy load admin pages - only loaded when admin users access them
const AdminPage = lazy(() => import(/* webpackChunkName: "admin" */ "./pages/AdminPage.js")); const AdminPage = lazy(() => import(/* webpackChunkName: "admin" */ "./pages/AdminPage.js"));
@@ -201,16 +205,25 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
<Header active categoryId={categoryId} key={authVersion} /> <Header active categoryId={categoryId} key={authVersion} />
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Suspense fallback={ <Suspense fallback={
<Box // Use prerender fallback if available, otherwise show loading spinner
sx={{ typeof window !== "undefined" && window.__PRERENDER_FALLBACK__ ? (
display: "flex", <div
justifyContent: "center", dangerouslySetInnerHTML={{
alignItems: "center", __html: window.__PRERENDER_FALLBACK__.content,
minHeight: "60vh", }}
}} />
> ) : (
<CircularProgress color="primary" /> <Box
</Box> sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "60vh",
}}
>
<CircularProgress color="primary" />
</Box>
)
}> }>
<CarouselProvider> <CarouselProvider>
<Routes> <Routes>
@@ -280,7 +293,17 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
</Box> </Box>
{/* Conditionally render the Chat Assistant */} {/* Conditionally render the Chat Assistant */}
{isChatOpen && ( {isChatOpen && (
<Suspense fallback={<CircularProgress size={20} />}> <Suspense fallback={
typeof window !== "undefined" && window.__PRERENDER_FALLBACK__ ? (
<div
dangerouslySetInnerHTML={{
__html: window.__PRERENDER_FALLBACK__.content,
}}
/>
) : (
<CircularProgress size={20} />
)
}>
<ChatAssistant <ChatAssistant
open={isChatOpen} open={isChatOpen}
onClose={handleChatClose} onClose={handleChatClose}
@@ -344,7 +367,17 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
{/* Development-only Theme Customizer Dialog */} {/* Development-only Theme Customizer Dialog */}
{isDevelopment && isThemeCustomizerOpen && ( {isDevelopment && isThemeCustomizerOpen && (
<Suspense fallback={<CircularProgress size={20} />}> <Suspense fallback={
typeof window !== "undefined" && window.__PRERENDER_FALLBACK__ ? (
<div
dangerouslySetInnerHTML={{
__html: window.__PRERENDER_FALLBACK__.content,
}}
/>
) : (
<CircularProgress size={20} />
)
}>
<ThemeCustomizerDialog <ThemeCustomizerDialog
open={isThemeCustomizerOpen} open={isThemeCustomizerOpen}
onClose={() => setThemeCustomizerOpen(false)} onClose={() => setThemeCustomizerOpen(false)}
@@ -382,16 +415,24 @@ const App = () => {
<SocketProvider <SocketProvider
url={config.apiBaseUrl} url={config.apiBaseUrl}
fallback={ fallback={
<Box typeof window !== "undefined" && window.__PRERENDER_FALLBACK__ ? (
sx={{ <div
display: "flex", dangerouslySetInnerHTML={{
justifyContent: "center", __html: window.__PRERENDER_FALLBACK__.content,
alignItems: "center", }}
height: "100vh", />
}} ) : (
> <Box
<CircularProgress color="primary" /> sx={{
</Box> display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
}}
>
<CircularProgress color="primary" />
</Box>
)
} }
> >
<AppContent <AppContent

View File

@@ -71,12 +71,13 @@ class PrerenderProduct extends React.Component {
React.createElement( React.createElement(
AppBar, AppBar,
{ position: 'sticky', color: 'primary', elevation: 0, sx: { zIndex: 1100 } }, { position: 'sticky', color: 'primary', elevation: 0, sx: { zIndex: 1100 } },
React.createElement( React.createElement(
Toolbar, Toolbar,
{ sx: { minHeight: 64, py: { xs: 0.5, sm: 0 } } }, { sx: { minHeight: 64, py: { xs: 0.5, sm: 0 } } },
React.createElement( React.createElement(
Container, Box,
{ maxWidth: { xs: false, sm: 'lg' }, sx: { display: 'flex', alignItems: 'center', px: { xs: 0, sm: 3 }, width: '100%' } }, { sx: { display: 'flex', alignItems: 'center', px: { xs: 0, sm: 3 }, maxWidth: { xs: 'none', sm: '1200px' }, mx: { xs: 0, sm: 'auto' }, width: '100%' } },
// Desktop: simple layout, Mobile: column layout with SearchBar space
React.createElement( React.createElement(
Box, Box,
{ {
@@ -84,56 +85,36 @@ class PrerenderProduct extends React.Component {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
width: '100%', width: '100%',
maxWidth: '100%', flexDirection: { xs: 'column', sm: 'row' }
flexDirection: { xs: 'column', sm: 'row' },
overflow: 'hidden'
} }
}, },
React.createElement( // First row: Logo (and ButtonGroup on mobile in SPA, but we don't need ButtonGroup in prerender)
Box, React.createElement(
{ Box,
sx: { {
display: 'flex', sx: {
alignItems: 'center', display: 'flex',
width: '100%', alignItems: 'center',
maxWidth: '100%', width: '100%',
justifyContent: { xs: 'space-between', sm: 'flex-start' }, justifyContent: { xs: 'flex-start', sm: 'flex-start' }, // Keep desktop simple
minHeight: { xs: 52, sm: 'auto' }, minHeight: { xs: 52, sm: 'auto' },
px: { xs: 0, sm: 0 }, px: { xs: 0, sm: 0 }
overflow: 'hidden' }
} },
}, React.createElement(Logo)
React.createElement(Logo), ),
// Invisible ButtonGroup placeholder for mobile layout // Second row: SearchBar placeholder only on mobile
React.createElement(
Box,
{
sx: {
display: { xs: 'flex', sm: 'none' },
alignItems: 'flex-end',
transform: 'translateY(4px) translateX(9px)',
ml: 0,
visibility: 'hidden',
width: 120, // Approximate width of ButtonGroup
height: 40
}
}
)
),
// Invisible SearchBar placeholder for mobile - matches SPA layout
React.createElement( React.createElement(
Box, Box,
{ {
sx: { sx: {
display: { xs: 'block', sm: 'none' }, display: { xs: 'block', sm: 'none' },
width: '100%', width: '100%',
maxWidth: '100%',
mt: { xs: 1, sm: 0 }, mt: { xs: 1, sm: 0 },
mb: { xs: 0.5, sm: 0 }, mb: { xs: 0.5, sm: 0 },
px: { xs: 0, sm: 0 }, px: { xs: 0, sm: 0 },
height: 41, // Small TextField height height: 41, // Small TextField height
visibility: 'hidden', visibility: 'hidden'
overflow: 'hidden'
} }
} }
) )

View File

@@ -20,7 +20,7 @@ class CartItem extends Component {
this.setState({image:window.tinyPicCache[picid],loading:false, error: false}) this.setState({image:window.tinyPicCache[picid],loading:false, error: false})
}else{ }else{
this.setState({image: null, loading: true, error: false}); this.setState({image: null, loading: true, error: false});
if(this.props.socket){ if(this.props.socket && this.props.socket.connected){
this.props.socket.emit('getPic', { bildId:picid, size:'tiny' }, (res) => { this.props.socket.emit('getPic', { bildId:picid, size:'tiny' }, (res) => {
if(res.success){ if(res.success){
window.tinyPicCache[picid] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' })); window.tinyPicCache[picid] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));

View File

@@ -12,7 +12,7 @@ import LoupeIcon from '@mui/icons-material/Loupe';
class Images extends Component { class Images extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { mainPic:0,pics:[]}; this.state = { mainPic:0,pics:[], needsSocketRetry: false };
} }
componentDidMount () { componentDidMount () {
@@ -22,6 +22,15 @@ class Images extends Component {
if (prevProps.fullscreenOpen !== this.props.fullscreenOpen) { if (prevProps.fullscreenOpen !== this.props.fullscreenOpen) {
this.updatePics(); this.updatePics();
} }
// Retry loading images if socket just became available
const wasConnected = prevProps.socketB && prevProps.socketB.connected;
const isNowConnected = this.props.socketB && this.props.socketB.connected;
if (!wasConnected && isNowConnected && this.state.needsSocketRetry) {
this.setState({ needsSocketRetry: false });
this.updatePics();
}
} }
updatePics = (newMainPic = this.state.mainPic) => { updatePics = (newMainPic = this.state.mainPic) => {
@@ -49,10 +58,10 @@ class Images extends Component {
pics.push(window.smallPicCache[bildId]); pics.push(window.smallPicCache[bildId]);
this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic); this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic);
}else if(window.tinyPicCache[bildId]){ }else if(window.tinyPicCache[bildId]){
pics.push(bildId); pics.push(window.tinyPicCache[bildId]);
this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic); this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic);
}else{ }else{
pics.push(bildId); pics.push(`/assets/images/prod${bildId}.jpg`);
this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic); this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic);
} }
}else{ }else{
@@ -67,7 +76,8 @@ class Images extends Component {
} }
} }
} }
console.log('pics',pics); console.log('DEBUG: pics array contents:', pics);
console.log('DEBUG: pics array types:', pics.map(p => typeof p + ': ' + p));
this.setState({ pics, mainPic: newMainPic }); this.setState({ pics, mainPic: newMainPic });
}else{ }else{
if(this.state.pics.length > 0) this.setState({ pics:[], mainPic: newMainPic }); if(this.state.pics.length > 0) this.setState({ pics:[], mainPic: newMainPic });
@@ -75,6 +85,13 @@ class Images extends Component {
} }
loadPic = (size,bildId,index) => { loadPic = (size,bildId,index) => {
// Check if socketB is available and connected before emitting
if (!this.props.socketB || !this.props.socketB.connected) {
console.log("Images: socketB not available, will retry when connected");
this.setState({ needsSocketRetry: true });
return;
}
this.props.socketB.emit('getPic', { bildId, size }, (res) => { this.props.socketB.emit('getPic', { bildId, size }, (res) => {
if(res.success){ if(res.success){
const url = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' })); const url = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));

View File

@@ -28,25 +28,16 @@ class Product extends Component {
}else{ }else{
this.state = {image: null, loading: true, error: false}; this.state = {image: null, loading: true, error: false};
console.log("Product: Fetching image from socketB", this.props.socketB); console.log("Product: Fetching image from socketB", this.props.socketB);
this.props.socketB.emit('getPic', { bildId, size:'small' }, (res) => {
if(res.success){ // Check if socketB is available and connected before emitting
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' })); if (this.props.socketB && this.props.socketB.connected) {
if (this._isMounted) { this.loadImage(bildId);
this.setState({image: window.smallPicCache[bildId], loading: false}); } else {
} else { // Socket not available, set error state or wait
this.state.image = window.smallPicCache[bildId]; console.log("Product: socketB not available, will retry when connected");
this.state.loading = false; this.state.error = true;
} this.state.loading = false;
}else{ }
console.log('Fehler beim Laden des Bildes:', res);
if (this._isMounted) {
this.setState({error: true, loading: false});
} else {
this.state.error = true;
this.state.loading = false;
}
}
})
} }
}else{ }else{
this.state = {image: null, loading: false, error: false}; this.state = {image: null, loading: false, error: false};
@@ -57,6 +48,45 @@ class Product extends Component {
this._isMounted = true; this._isMounted = true;
} }
componentDidUpdate(prevProps) {
// Retry loading image if socket just became available
const wasConnected = prevProps.socketB && prevProps.socketB.connected;
const isNowConnected = this.props.socketB && this.props.socketB.connected;
if (!wasConnected && isNowConnected && this.state.error && this.props.pictureList) {
// Socket just connected and we had an error, retry loading
const bildId = this.props.pictureList.split(',')[0];
if (!window.smallPicCache[bildId]) {
this.setState({loading: true, error: false});
this.loadImage(bildId);
}
}
}
loadImage = (bildId) => {
if (this.props.socketB && this.props.socketB.connected) {
this.props.socketB.emit('getPic', { bildId, size:'small' }, (res) => {
if(res.success){
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
if (this._isMounted) {
this.setState({image: window.smallPicCache[bildId], loading: false});
} else {
this.state.image = window.smallPicCache[bildId];
this.state.loading = false;
}
}else{
console.log('Fehler beim Laden des Bildes:', res);
if (this._isMounted) {
this.setState({error: true, loading: false});
} else {
this.state.error = true;
this.state.loading = false;
}
}
});
}
}
componentWillUnmount() { componentWillUnmount() {
this._isMounted = false; this._isMounted = false;
} }

View File

@@ -356,7 +356,6 @@ class ProductDetailPage extends Component {
if (!this.props.socket || !this.props.socket.connected) { if (!this.props.socket || !this.props.socket.connected) {
// Socket not connected yet, but don't show error immediately on first load // Socket not connected yet, but don't show error immediately on first load
// The componentDidUpdate will retry when socket connects // The componentDidUpdate will retry when socket connects
console.log("Socket not connected yet, waiting for connection to load product data");
return; return;
} }
@@ -538,11 +537,27 @@ class ProductDetailPage extends Component {
} }
}; };
render() { render() {
const { product, loading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } = const { product, loading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } =
this.state; this.state;
// Debug alerts removed
if (loading) { if (loading) {
// Check if prerender fallback is available
if (typeof window !== "undefined" && window.__PRERENDER_FALLBACK__) {
return (
<div
dangerouslySetInnerHTML={{
__html: window.__PRERENDER_FALLBACK__.content,
}}
/>
);
}
// Fallback to loading message if no prerender content
return ( return (
<Box <Box
sx={{ sx={{

View File

@@ -257,7 +257,7 @@ const SearchBar = () => {
), ),
endAdornment: ( endAdornment: (
<InputAdornment position="end"> <InputAdornment position="end">
{loadingSuggestions && <CircularProgress size={16} />} {loadingSuggestions && <CircularProgress size={16} />}
<IconButton <IconButton
size="small" size="small"
onClick={handleEnterClick} onClick={handleEnterClick}