diff --git a/prerender/seo/homepage.cjs b/prerender/seo/homepage.cjs index 2a79206..812685d 100644 --- a/prerender/seo/homepage.cjs +++ b/prerender/seo/homepage.cjs @@ -7,6 +7,8 @@ const generateHomepageMetaTags = (baseUrl, config) => { const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; return ` + + @@ -233,4 +235,4 @@ const generateHomepageJsonLd = (baseUrl, config, categories = []) => { module.exports = { generateHomepageMetaTags, generateHomepageJsonLd, -}; \ No newline at end of file +}; diff --git a/src/App.js b/src/App.js index 214816d..6856ba9 100644 --- a/src/App.js +++ b/src/App.js @@ -32,11 +32,10 @@ import i18n from './i18n/index.js'; import Header from "./components/Header.js"; import Footer from "./components/Footer.js"; -import MainPageLayout from "./components/MainPageLayout.js"; -import IdleMainPagesSlideshow from "./components/IdleMainPagesSlideshow.js"; - -import Content from "./components/Content.js"; -import ProductDetail from "./components/ProductDetail.js"; +const IdleMainPagesSlideshow = lazy(() => import(/* webpackChunkName: "idle-slideshow" */ "./components/IdleMainPagesSlideshow.js")); +const MainPageLayout = lazy(() => import(/* webpackChunkName: "main-page-layout" */ "./components/MainPageLayout.js")); +const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js")); +const ProductDetail = lazy(() => import(/* webpackChunkName: "product-detail" */ "./components/ProductDetail.js")); // Lazy load rarely-accessed pages const ProfilePage = lazy(() => import(/* webpackChunkName: "profile" */ "./pages/ProfilePage.js")); diff --git a/src/PrerenderHome.js b/src/PrerenderHome.js index 0496e55..0d37973 100644 --- a/src/PrerenderHome.js +++ b/src/PrerenderHome.js @@ -7,6 +7,8 @@ import { } from '@mui/material'; import Footer from './components/Footer.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 { @@ -122,9 +124,18 @@ class PrerenderHome extends React.Component { ), 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) ); } } -export default PrerenderHome; \ No newline at end of file +export default PrerenderHome; diff --git a/src/components/ChatAssistant.js b/src/components/ChatAssistant.js index 05da261..3f96556 100644 --- a/src/components/ChatAssistant.js +++ b/src/components/ChatAssistant.js @@ -18,7 +18,7 @@ import { Link } from 'react-router-dom'; import MuiLink from '@mui/material/Link'; import { alpha } from '@mui/material/styles'; import TelegramIcon from '@mui/icons-material/Telegram'; -import { isUserLoggedIn } from './LoginComponent.js'; +import { isUserLoggedIn } from '../utils/authSession.js'; import { withTranslation } from '../i18n/withTranslation.js'; const TELEGRAM_ASSISTANT_URL = 'https://t.me/Growheads_de_Bot'; diff --git a/src/components/LanguageSwitcher.js b/src/components/LanguageSwitcher.js index cb2974a..10cff1c 100644 --- a/src/components/LanguageSwitcher.js +++ b/src/components/LanguageSwitcher.js @@ -6,13 +6,37 @@ import MenuItem from '@mui/material/MenuItem'; import Typography from '@mui/material/Typography'; 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 { constructor(props) { super(props); - + this.state = { - anchorEl: null, - loadedFlags: {} + anchorEl: null }; } @@ -36,72 +60,7 @@ class LanguageSwitcher extends Component { 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) => { - const FlagComponent = this.state.loadedFlags[lang]; - - if (FlagComponent) { - return ( - - ); - } - - // Loading placeholder or fallback return ( - {this.getLanguageLabel(lang)} + {LANGUAGE_FLAG_EMOJIS[lang] || this.getLanguageLabel(lang)} ); }; - // 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) => { const labels = { 'ar': 'EG', @@ -275,4 +216,4 @@ class LanguageSwitcher extends Component { } } -export default withI18n()(LanguageSwitcher); \ No newline at end of file +export default withI18n()(LanguageSwitcher); diff --git a/src/components/LoginComponent.js b/src/components/LoginComponent.js index a2f6656..18bd712 100644 --- a/src/components/LoginComponent.js +++ b/src/components/LoginComponent.js @@ -21,6 +21,7 @@ import { withRouter } from './withRouter.js'; import GoogleLoginButton from './GoogleLoginButton.js'; import CartSyncDialog from './CartSyncDialog.js'; import { localAndArchiveServer, mergeCarts } from '../utils/cartUtils.js'; +import { isUserLoggedIn } from '../utils/authSession.js'; import config from '../config.js'; import { withI18n } from '../i18n/withTranslation.js'; 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 function cartsAreIdentical(cartA, cartB) { console.log('Vergleiche Carts:', {cartA, cartB}); @@ -809,4 +792,4 @@ export class LoginComponent extends Component { } } -export default withRouter(withI18n()(LoginComponent)); \ No newline at end of file +export default withRouter(withI18n()(LoginComponent)); diff --git a/src/components/MainPageLayout.js b/src/components/MainPageLayout.js index 33a982b..7778a18 100644 --- a/src/components/MainPageLayout.js +++ b/src/components/MainPageLayout.js @@ -275,7 +275,15 @@ const ContentBox = ({ box, index, pageType, starHovered, setStarHovered, opacity > {opacity === 1 && ( - {box.title} + {box.title} )} {box.title} diff --git a/src/components/header/ButtonGroup.js b/src/components/header/ButtonGroup.js index 3b98517..a7dec0b 100644 --- a/src/components/header/ButtonGroup.js +++ b/src/components/header/ButtonGroup.js @@ -18,8 +18,8 @@ const LoginComponent = lazy(() => import(/* webpackChunkName: "login" */ "../Log import CartDropdown from '../CartDropdown.js'; import LanguageSwitcher from '../LanguageSwitcher.js'; -import { isUserLoggedIn } from '../LoginComponent.js'; import { withI18n } from '../../i18n/withTranslation.js'; +import { isUserLoggedIn } from '../../utils/authSession.js'; function getBadgeNumber() { let count = 0; @@ -196,4 +196,4 @@ const ButtonGroupWithRouter = (props) => { return ; }; -export default ButtonGroupWithRouter; \ No newline at end of file +export default ButtonGroupWithRouter; diff --git a/src/components/profile/OrderProcessingService.js b/src/components/profile/OrderProcessingService.js index 2723047..6e791a3 100644 --- a/src/components/profile/OrderProcessingService.js +++ b/src/components/profile/OrderProcessingService.js @@ -1,4 +1,4 @@ -import { isUserLoggedIn } from "../LoginComponent.js"; +import { isUserLoggedIn } from "../../utils/authSession.js"; class OrderProcessingService { constructor(getContext, setState) { @@ -369,4 +369,4 @@ class OrderProcessingService { } } -export default OrderProcessingService; \ No newline at end of file +export default OrderProcessingService; diff --git a/src/utils/authSession.js b/src/utils/authSession.js new file mode 100644 index 0000000..9afec43 --- /dev/null +++ b/src/utils/authSession.js @@ -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 }; +}; diff --git a/webpack.config.js b/webpack.config.js index 4d574ee..0418305 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,17 +5,31 @@ import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import ESLintPlugin from 'eslint-webpack-plugin'; import { cpSync } from 'fs'; -import { execSync } from 'child_process'; +import { execFileSync } from 'child_process'; import webpack from 'webpack'; import fs from 'fs'; 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 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 { - return execSync('git rev-parse HEAD').toString().trim(); + return execFileSync('git', ['rev-parse', 'HEAD'], { + encoding: 'utf8', + maxBuffer: 1024 * 1024, + }).trim(); } 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'; } }; @@ -301,9 +315,36 @@ export default { priority: 20, 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 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', priority: 15, chunks: 'async',