refactor: Update webpack configuration to improve git commit hash retrieval and enhance lazy loading of components for better performance

This commit is contained in:
sebseb7
2026-03-28 17:56:12 +01:00
parent ab55761411
commit 52c9888a6a
11 changed files with 129 additions and 128 deletions

View File

@@ -7,6 +7,8 @@ const generateHomepageMetaTags = (baseUrl, config) => {
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
return ` return `
<link rel="preload" as="image" href="/assets/images/konfigurator.avif" fetchpriority="high">
<!-- SEO Meta Tags --> <!-- SEO Meta Tags -->
<meta name="description" content="${description}"> <meta name="description" content="${description}">
<meta name="keywords" content="${keywords}"> <meta name="keywords" content="${keywords}">

View File

@@ -32,11 +32,10 @@ import i18n from './i18n/index.js';
import Header from "./components/Header.js"; import Header from "./components/Header.js";
import Footer from "./components/Footer.js"; import Footer from "./components/Footer.js";
import MainPageLayout from "./components/MainPageLayout.js"; const IdleMainPagesSlideshow = lazy(() => import(/* webpackChunkName: "idle-slideshow" */ "./components/IdleMainPagesSlideshow.js"));
import IdleMainPagesSlideshow from "./components/IdleMainPagesSlideshow.js"; const MainPageLayout = lazy(() => import(/* webpackChunkName: "main-page-layout" */ "./components/MainPageLayout.js"));
const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js"));
import Content from "./components/Content.js"; const ProductDetail = lazy(() => import(/* webpackChunkName: "product-detail" */ "./components/ProductDetail.js"));
import ProductDetail from "./components/ProductDetail.js";
// Lazy load rarely-accessed pages // Lazy load rarely-accessed pages
const ProfilePage = lazy(() => import(/* webpackChunkName: "profile" */ "./pages/ProfilePage.js")); const ProfilePage = lazy(() => import(/* webpackChunkName: "profile" */ "./pages/ProfilePage.js"));

View File

@@ -7,6 +7,8 @@ import {
} from '@mui/material'; } from '@mui/material';
import Footer from './components/Footer.js'; import Footer from './components/Footer.js';
import { Logo, CategoryList } from './components/header/index.js'; import { Logo, CategoryList } from './components/header/index.js';
import MainPageLayout from './components/MainPageLayout.js';
import { CarouselProvider } from './contexts/CarouselContext.js';
class PrerenderHome extends React.Component { class PrerenderHome extends React.Component {
@@ -122,6 +124,15 @@ class PrerenderHome extends React.Component {
), ),
React.createElement(CategoryList, { categoryId: 209, activeCategoryId: null }) React.createElement(CategoryList, { categoryId: 209, activeCategoryId: null })
), ),
React.createElement(
Box,
{ component: 'main', sx: { flexGrow: 1 } },
React.createElement(
CarouselProvider,
null,
React.createElement(MainPageLayout)
)
),
React.createElement(Footer) React.createElement(Footer)
); );
} }

View File

@@ -18,7 +18,7 @@ import { Link } from 'react-router-dom';
import MuiLink from '@mui/material/Link'; import MuiLink from '@mui/material/Link';
import { alpha } from '@mui/material/styles'; import { alpha } from '@mui/material/styles';
import TelegramIcon from '@mui/icons-material/Telegram'; import TelegramIcon from '@mui/icons-material/Telegram';
import { isUserLoggedIn } from './LoginComponent.js'; import { isUserLoggedIn } from '../utils/authSession.js';
import { withTranslation } from '../i18n/withTranslation.js'; import { withTranslation } from '../i18n/withTranslation.js';
const TELEGRAM_ASSISTANT_URL = 'https://t.me/Growheads_de_Bot'; const TELEGRAM_ASSISTANT_URL = 'https://t.me/Growheads_de_Bot';

View File

@@ -6,13 +6,37 @@ import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { withI18n } from '../i18n/withTranslation.js'; import { withI18n } from '../i18n/withTranslation.js';
const LANGUAGE_FLAG_EMOJIS = {
ar: '🇪🇬',
bg: '🇧🇬',
cs: '🇨🇿',
de: '🇩🇪',
el: '🇬🇷',
en: '🇺🇸',
es: '🇪🇸',
fr: '🇫🇷',
hr: '🇭🇷',
hu: '🇭🇺',
it: '🇮🇹',
pl: '🇵🇱',
ro: '🇷🇴',
ru: '🇷🇺',
sk: '🇸🇰',
sl: '🇸🇮',
sq: '🇦🇱',
sr: '🇷🇸',
sv: '🇸🇪',
tr: '🇹🇷',
uk: '🇺🇦',
zh: '🇨🇳'
};
class LanguageSwitcher extends Component { class LanguageSwitcher extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
anchorEl: null, anchorEl: null
loadedFlags: {}
}; };
} }
@@ -36,72 +60,7 @@ class LanguageSwitcher extends Component {
this.handleClose(); this.handleClose();
}; };
// Lazy load flag components
loadFlagComponent = async (lang) => {
if (this.state.loadedFlags[lang]) {
return this.state.loadedFlags[lang];
}
try {
const flagMap = {
'ar': () => import('country-flag-icons/react/3x2').then(m => m.EG),
'bg': () => import('country-flag-icons/react/3x2').then(m => m.BG),
'cs': () => import('country-flag-icons/react/3x2').then(m => m.CZ),
'de': () => import('country-flag-icons/react/3x2').then(m => m.DE),
'el': () => import('country-flag-icons/react/3x2').then(m => m.GR),
'en': () => import('country-flag-icons/react/3x2').then(m => m.US),
'es': () => import('country-flag-icons/react/3x2').then(m => m.ES),
'fr': () => import('country-flag-icons/react/3x2').then(m => m.FR),
'hr': () => import('country-flag-icons/react/3x2').then(m => m.HR),
'hu': () => import('country-flag-icons/react/3x2').then(m => m.HU),
'it': () => import('country-flag-icons/react/3x2').then(m => m.IT),
'pl': () => import('country-flag-icons/react/3x2').then(m => m.PL),
'ro': () => import('country-flag-icons/react/3x2').then(m => m.RO),
'ru': () => import('country-flag-icons/react/3x2').then(m => m.RU),
'sk': () => import('country-flag-icons/react/3x2').then(m => m.SK),
'sl': () => import('country-flag-icons/react/3x2').then(m => m.SI),
'sq': () => import('country-flag-icons/react/3x2').then(m => m.AL),
'sr': () => import('country-flag-icons/react/3x2').then(m => m.RS),
'sv': () => import('country-flag-icons/react/3x2').then(m => m.SE),
'tr': () => import('country-flag-icons/react/3x2').then(m => m.TR),
'uk': () => import('country-flag-icons/react/3x2').then(m => m.UA),
'zh': () => import('country-flag-icons/react/3x2').then(m => m.CN)
};
const flagLoader = flagMap[lang];
if (flagLoader) {
const FlagComponent = await flagLoader();
this.setState(prevState => ({
loadedFlags: {
...prevState.loadedFlags,
[lang]: FlagComponent
}
}));
return FlagComponent;
}
} catch (error) {
console.warn(`Failed to load flag for language: ${lang}`, error);
}
return null;
};
getLanguageFlag = (lang) => { getLanguageFlag = (lang) => {
const FlagComponent = this.state.loadedFlags[lang];
if (FlagComponent) {
return (
<FlagComponent
style={{
width: '20px',
height: '14px',
borderRadius: '2px',
border: '1px solid #ddd'
}}
/>
);
}
// Loading placeholder or fallback
return ( return (
<Box <Box
component="span" component="span"
@@ -111,35 +70,17 @@ class LanguageSwitcher extends Component {
justifyContent: 'center', justifyContent: 'center',
minWidth: '20px', minWidth: '20px',
height: '14px', height: '14px',
backgroundColor: '#f5f5f5', fontSize: '14px',
color: '#666', lineHeight: 1,
fontSize: '8px',
fontWeight: 'bold',
borderRadius: '2px', borderRadius: '2px',
fontFamily: 'monospace', overflow: 'hidden'
border: '1px solid #ddd'
}} }}
> >
{this.getLanguageLabel(lang)} {LANGUAGE_FLAG_EMOJIS[lang] || this.getLanguageLabel(lang)}
</Box> </Box>
); );
}; };
// Load flags when menu opens
componentDidUpdate(prevProps, prevState) {
const { anchorEl } = this.state;
const { languageContext } = this.props;
if (anchorEl && !prevState.anchorEl && languageContext) {
// Menu just opened, lazy load flags for all languages (not just available ones)
languageContext.allLanguages.forEach(lang => {
if (!this.state.loadedFlags[lang]) {
this.loadFlagComponent(lang);
}
});
}
}
getLanguageLabel = (lang) => { getLanguageLabel = (lang) => {
const labels = { const labels = {
'ar': 'EG', 'ar': 'EG',

View File

@@ -21,6 +21,7 @@ import { withRouter } from './withRouter.js';
import GoogleLoginButton from './GoogleLoginButton.js'; import GoogleLoginButton from './GoogleLoginButton.js';
import CartSyncDialog from './CartSyncDialog.js'; import CartSyncDialog from './CartSyncDialog.js';
import { localAndArchiveServer, mergeCarts } from '../utils/cartUtils.js'; import { localAndArchiveServer, mergeCarts } from '../utils/cartUtils.js';
import { isUserLoggedIn } from '../utils/authSession.js';
import config from '../config.js'; import config from '../config.js';
import { withI18n } from '../i18n/withTranslation.js'; import { withI18n } from '../i18n/withTranslation.js';
import { import {
@@ -53,24 +54,6 @@ const persistSessionAuth = (response) => {
} }
}; };
// Function to check if user is logged in
export const isUserLoggedIn = () => {
const storedUser = sessionStorage.getItem('user');
if (storedUser) {
try {
const parsedUser = JSON.parse(storedUser);
console.log('Parsed User:', parsedUser);
return { isLoggedIn: true, user: parsedUser, isAdmin: !!parsedUser.admin };
} catch (error) {
console.error('Error parsing user from sessionStorage:', error);
sessionStorage.removeItem('user');
}
}
console.log('isUserLoggedIn', false);
return { isLoggedIn: false, user: null, isAdmin: false };
};
// Hilfsfunktion zum Vergleich zweier Cart-Arrays // Hilfsfunktion zum Vergleich zweier Cart-Arrays
function cartsAreIdentical(cartA, cartB) { function cartsAreIdentical(cartA, cartB) {
console.log('Vergleiche Carts:', {cartA, cartB}); console.log('Vergleiche Carts:', {cartA, cartB});

View File

@@ -275,7 +275,15 @@ const ContentBox = ({ box, index, pageType, starHovered, setStarHovered, opacity
> >
<Box sx={{ height: "100%", bgcolor: box.bgcolor, position: "relative", display: "flex", alignItems: "center", justifyContent: "center" }}> <Box sx={{ height: "100%", bgcolor: box.bgcolor, position: "relative", display: "flex", alignItems: "center", justifyContent: "center" }}>
{opacity === 1 && ( {opacity === 1 && (
<img src={box.image} alt={box.title} style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain", position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)" }} /> <img
src={box.image}
alt={box.title}
loading={pageType === "home" && index === 1 ? "eager" : undefined}
fetchPriority={pageType === "home" && index === 1 ? "high" : undefined}
width={pageType === "home" && index === 1 ? 768 : undefined}
height={pageType === "home" && index === 1 ? 650 : undefined}
style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain", position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)" }}
/>
)} )}
<Box sx={{ position: "absolute", bottom: 0, left: 0, right: 0, bgcolor: "rgba(27, 94, 32, 0.8)", p: 1 }}> <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> <Typography sx={{ fontSize: "1.6rem", color: "white", fontFamily: "SwashingtonCP" }}>{box.title}</Typography>

View File

@@ -18,8 +18,8 @@ const LoginComponent = lazy(() => import(/* webpackChunkName: "login" */ "../Log
import CartDropdown from '../CartDropdown.js'; import CartDropdown from '../CartDropdown.js';
import LanguageSwitcher from '../LanguageSwitcher.js'; import LanguageSwitcher from '../LanguageSwitcher.js';
import { isUserLoggedIn } from '../LoginComponent.js';
import { withI18n } from '../../i18n/withTranslation.js'; import { withI18n } from '../../i18n/withTranslation.js';
import { isUserLoggedIn } from '../../utils/authSession.js';
function getBadgeNumber() { function getBadgeNumber() {
let count = 0; let count = 0;

View File

@@ -1,4 +1,4 @@
import { isUserLoggedIn } from "../LoginComponent.js"; import { isUserLoggedIn } from "../../utils/authSession.js";
class OrderProcessingService { class OrderProcessingService {
constructor(getContext, setState) { constructor(getContext, setState) {

16
src/utils/authSession.js Normal file
View File

@@ -0,0 +1,16 @@
export const isUserLoggedIn = () => {
const storedUser = sessionStorage.getItem('user');
if (storedUser) {
try {
const parsedUser = JSON.parse(storedUser);
console.log('Parsed User:', parsedUser);
return { isLoggedIn: true, user: parsedUser, isAdmin: !!parsedUser.admin };
} catch (error) {
console.error('Error parsing user from sessionStorage:', error);
sessionStorage.removeItem('user');
}
}
console.log('isUserLoggedIn', false);
return { isLoggedIn: false, user: null, isAdmin: false };
};

View File

@@ -5,17 +5,31 @@ import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import ESLintPlugin from 'eslint-webpack-plugin'; import ESLintPlugin from 'eslint-webpack-plugin';
import { cpSync } from 'fs'; import { cpSync } from 'fs';
import { execSync } from 'child_process'; import { execFileSync } from 'child_process';
import webpack from 'webpack'; import webpack from 'webpack';
import fs from 'fs'; import fs from 'fs';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
// Get git commit hash // Git hash for meta tag / currentHash.json — avoid execSync (spawns /bin/sh; fails with EPERM in some sandboxes/CI)
const getGitCommitHash = () => { const getGitCommitHash = () => {
const fromEnv =
process.env.GIT_COMMIT ||
process.env.VERCEL_GIT_COMMIT_SHA ||
process.env.CI_COMMIT_SHA ||
process.env.GITHUB_SHA ||
'';
if (fromEnv) return String(fromEnv).trim();
try { try {
return execSync('git rev-parse HEAD').toString().trim(); return execFileSync('git', ['rev-parse', 'HEAD'], {
encoding: 'utf8',
maxBuffer: 1024 * 1024,
}).trim();
} catch (e) { } catch (e) {
console.error('Failed to get git commit hash:', e); console.warn(
'Git commit hash unavailable (set GIT_COMMIT or run build in a git repo):',
e && e.message ? e.message : e
);
return 'unknown'; return 'unknown';
} }
}; };
@@ -301,9 +315,36 @@ export default {
priority: 20, priority: 20,
reuseExistingChunk: true, reuseExistingChunk: true,
}, },
// Keep Stripe checkout code out of the initial vendor bundle
stripe: {
test: /[\\/]node_modules[\\/]@stripe[\\/]/,
name: 'stripe',
priority: 19,
chunks: 'async',
reuseExistingChunk: true,
enforce: true,
},
// Lazy HTML parsing/sanitizing should stay async with the product detail/chat flows
htmlParser: {
test: /[\\/]node_modules[\\/](html-react-parser|sanitize-html|htmlparser2|domhandler|dom-serializer|entities|react-property|parse-srcset|postcss)[\\/]/,
name: 'html-parser-vendor',
priority: 18,
chunks: 'async',
reuseExistingChunk: true,
enforce: true,
},
// Girocode/QR code generation is only needed in checkout/profile flows
payments: {
test: /[\\/]node_modules[\\/](qrcode|sepa-payment-qr-code|iban)[\\/]/,
name: 'payments',
priority: 17,
chunks: 'async',
reuseExistingChunk: true,
enforce: true,
},
// socket.io-client and its dependencies — always async, never initial // socket.io-client and its dependencies — always async, never initial
socketio: { socketio: {
test: /[\\/]node_modules[\\/](socket\.io-client|engine\.io-client|@socket\.io|socket\.io-parser|socket\.io-msgpack-parser)[\\/]/, test: /[\\/]node_modules[\\/](socket\.io-client|engine\.io-client|engine\.io-parser|@socket\.io|socket\.io-parser|socket\.io-msgpack-parser)[\\/]/,
name: 'socketio', name: 'socketio',
priority: 15, priority: 15,
chunks: 'async', chunks: 'async',