feat(analytics-hubspot): modularize analytics and HubSpot integration
- Moved Google Analytics and HubSpot integration scripts to separate modules for better organization and maintainability. - Implemented an AnalyticsManager class to handle Google Analytics initialization and privacy notice display. - Created a HubSpotManager class to manage HubSpot widget loading and dark mode theming. - Updated index.html to reference the new modules, improving code clarity and separation of concerns.
This commit is contained in:
@@ -57,116 +57,15 @@
|
|||||||
<!-- Environment configuration -->
|
<!-- Environment configuration -->
|
||||||
<script src="/env-config.js"></script>
|
<script src="/env-config.js"></script>
|
||||||
|
|
||||||
<!-- Optimized Google Analytics with reduced blocking -->
|
<!-- Analytics Module -->
|
||||||
<script>
|
<script src="/js/analytics.js"></script>
|
||||||
// Function to initialize Google Analytics asynchronously
|
|
||||||
function initGoogleAnalytics() {
|
|
||||||
// Use requestIdleCallback to defer analytics loading
|
|
||||||
const loadAnalytics = () => {
|
|
||||||
// Determine which tracking ID to use based on the environment
|
|
||||||
const isProduction = window.location.hostname === 'app.worklenz.com';
|
|
||||||
|
|
||||||
const trackingId = isProduction ? 'G-7KSRKQ1397' : 'G-3LM2HGWEXG'; // Open source tracking ID
|
|
||||||
|
|
||||||
// Load the Google Analytics script
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.async = true;
|
|
||||||
script.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`;
|
|
||||||
document.head.appendChild(script);
|
|
||||||
|
|
||||||
// Initialize Google Analytics
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag() {
|
|
||||||
dataLayer.push(arguments);
|
|
||||||
}
|
|
||||||
gtag('js', new Date());
|
|
||||||
gtag('config', trackingId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use requestIdleCallback if available, otherwise setTimeout
|
|
||||||
if ('requestIdleCallback' in window) {
|
|
||||||
requestIdleCallback(loadAnalytics, { timeout: 2000 });
|
|
||||||
} else {
|
|
||||||
setTimeout(loadAnalytics, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize analytics after a delay to not block initial render
|
|
||||||
initGoogleAnalytics();
|
|
||||||
|
|
||||||
// Function to show privacy notice
|
|
||||||
function showPrivacyNotice() {
|
|
||||||
const notice = document.createElement('div');
|
|
||||||
notice.style.cssText = `
|
|
||||||
position: fixed;
|
|
||||||
bottom: 16px;
|
|
||||||
right: 16px;
|
|
||||||
background: #222;
|
|
||||||
color: #f5f5f5;
|
|
||||||
padding: 12px 16px 10px 16px;
|
|
||||||
border-radius: 7px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
|
||||||
z-index: 1000;
|
|
||||||
max-width: 320px;
|
|
||||||
font-family: Inter, sans-serif;
|
|
||||||
border: 1px solid #333;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
`;
|
|
||||||
notice.innerHTML = `
|
|
||||||
<div style="margin-bottom: 6px; font-weight: 600; color: #fff; font-size: 1rem;">Analytics Notice</div>
|
|
||||||
<div style="margin-bottom: 8px; color: #f5f5f5;">This app uses Google Analytics for anonymous usage stats. No personal data is tracked.</div>
|
|
||||||
<button id="analytics-notice-btn" style="padding: 5px 14px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.95rem;">Got it</button>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(notice);
|
|
||||||
// Add event listener to button
|
|
||||||
const btn = notice.querySelector('#analytics-notice-btn');
|
|
||||||
btn.addEventListener('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
localStorage.setItem('privacyNoticeShown', 'true');
|
|
||||||
notice.remove();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for DOM to be ready
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
// Check if we should show the notice
|
|
||||||
const isProduction =
|
|
||||||
window.location.hostname === 'worklenz.com' ||
|
|
||||||
window.location.hostname === 'app.worklenz.com';
|
|
||||||
const noticeShown = localStorage.getItem('privacyNoticeShown') === 'true';
|
|
||||||
|
|
||||||
// Show notice if not in production and not shown before
|
|
||||||
if (!isProduction && !noticeShown) {
|
|
||||||
showPrivacyNotice();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="./src/index.tsx"></script>
|
<script type="module" src="./src/index.tsx"></script>
|
||||||
<script type="text/javascript">
|
<!-- HubSpot Integration Module -->
|
||||||
// Load HubSpot script asynchronously and only for production
|
<script src="/js/hubspot.js"></script>
|
||||||
if (window.location.hostname === 'app.worklenz.com') {
|
|
||||||
// Use requestIdleCallback to defer HubSpot loading
|
|
||||||
const loadHubSpot = () => {
|
|
||||||
var hs = document.createElement('script');
|
|
||||||
hs.type = 'text/javascript';
|
|
||||||
hs.id = 'hs-script-loader';
|
|
||||||
hs.async = true;
|
|
||||||
hs.defer = true;
|
|
||||||
hs.src = '//js.hs-scripts.com/22348300.js';
|
|
||||||
document.body.appendChild(hs);
|
|
||||||
};
|
|
||||||
|
|
||||||
if ('requestIdleCallback' in window) {
|
|
||||||
requestIdleCallback(loadHubSpot, { timeout: 3000 });
|
|
||||||
} else {
|
|
||||||
setTimeout(loadHubSpot, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
97
worklenz-frontend/public/js/analytics.js
Normal file
97
worklenz-frontend/public/js/analytics.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Google Analytics initialization module
|
||||||
|
* Handles analytics loading and privacy notices
|
||||||
|
*/
|
||||||
|
|
||||||
|
class AnalyticsManager {
|
||||||
|
constructor() {
|
||||||
|
this.isProduction = window.location.hostname === 'app.worklenz.com';
|
||||||
|
this.trackingId = this.isProduction ? 'G-7KSRKQ1397' : 'G-3LM2HGWEXG';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Google Analytics asynchronously
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
const loadAnalytics = () => {
|
||||||
|
// Load the Google Analytics script
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.async = true;
|
||||||
|
script.src = `https://www.googletagmanager.com/gtag/js?id=${this.trackingId}`;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
// Initialize Google Analytics
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag() {
|
||||||
|
dataLayer.push(arguments);
|
||||||
|
}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', this.trackingId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use requestIdleCallback if available, otherwise setTimeout
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
requestIdleCallback(loadAnalytics, { timeout: 2000 });
|
||||||
|
} else {
|
||||||
|
setTimeout(loadAnalytics, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show privacy notice for non-production environments
|
||||||
|
*/
|
||||||
|
showPrivacyNotice() {
|
||||||
|
const notice = document.createElement('div');
|
||||||
|
notice.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
background: #222;
|
||||||
|
color: #f5f5f5;
|
||||||
|
padding: 12px 16px 10px 16px;
|
||||||
|
border-radius: 7px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
||||||
|
z-index: 1000;
|
||||||
|
max-width: 320px;
|
||||||
|
font-family: Inter, sans-serif;
|
||||||
|
border: 1px solid #333;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
`;
|
||||||
|
notice.innerHTML = `
|
||||||
|
<div style="margin-bottom: 6px; font-weight: 600; color: #fff; font-size: 1rem;">Analytics Notice</div>
|
||||||
|
<div style="margin-bottom: 8px; color: #f5f5f5;">This app uses Google Analytics for anonymous usage stats. No personal data is tracked.</div>
|
||||||
|
<button id="analytics-notice-btn" style="padding: 5px 14px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.95rem;">Got it</button>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(notice);
|
||||||
|
|
||||||
|
// Add event listener to button
|
||||||
|
const btn = notice.querySelector('#analytics-notice-btn');
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
localStorage.setItem('privacyNoticeShown', 'true');
|
||||||
|
notice.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if privacy notice should be shown
|
||||||
|
*/
|
||||||
|
checkPrivacyNotice() {
|
||||||
|
const isProduction =
|
||||||
|
window.location.hostname === 'worklenz.com' ||
|
||||||
|
window.location.hostname === 'app.worklenz.com';
|
||||||
|
const noticeShown = localStorage.getItem('privacyNoticeShown') === 'true';
|
||||||
|
|
||||||
|
// Show notice if not in production and not shown before
|
||||||
|
if (!isProduction && !noticeShown) {
|
||||||
|
this.showPrivacyNotice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize analytics when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const analytics = new AnalyticsManager();
|
||||||
|
analytics.init();
|
||||||
|
analytics.checkPrivacyNotice();
|
||||||
|
});
|
||||||
137
worklenz-frontend/public/js/hubspot.js
Normal file
137
worklenz-frontend/public/js/hubspot.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* HubSpot Chat Widget integration module
|
||||||
|
* Handles widget loading and dark mode theming
|
||||||
|
*/
|
||||||
|
|
||||||
|
class HubSpotManager {
|
||||||
|
constructor() {
|
||||||
|
this.isProduction = window.location.hostname === 'app.worklenz.com';
|
||||||
|
this.scriptId = 'hs-script-loader';
|
||||||
|
this.scriptSrc = '//js.hs-scripts.com/22348300.js';
|
||||||
|
this.styleId = 'hubspot-dark-mode-override';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load HubSpot script with dark mode support
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
if (!this.isProduction) return;
|
||||||
|
|
||||||
|
const loadHubSpot = () => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.type = 'text/javascript';
|
||||||
|
script.id = this.scriptId;
|
||||||
|
script.async = true;
|
||||||
|
script.defer = true;
|
||||||
|
script.src = this.scriptSrc;
|
||||||
|
|
||||||
|
// Configure dark mode after script loads
|
||||||
|
script.onload = () => this.setupDarkModeSupport();
|
||||||
|
|
||||||
|
document.body.appendChild(script);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use requestIdleCallback for better performance
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
requestIdleCallback(loadHubSpot, { timeout: 3000 });
|
||||||
|
} else {
|
||||||
|
setTimeout(loadHubSpot, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup dark mode theme switching for HubSpot widget
|
||||||
|
*/
|
||||||
|
setupDarkModeSupport() {
|
||||||
|
const applyTheme = () => {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
|
||||||
|
// Remove existing theme styles
|
||||||
|
const existingStyle = document.getElementById(this.styleId);
|
||||||
|
if (existingStyle) {
|
||||||
|
existingStyle.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDark) {
|
||||||
|
this.injectDarkModeCSS();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply initial theme after delay to ensure widget is loaded
|
||||||
|
setTimeout(applyTheme, 1000);
|
||||||
|
|
||||||
|
// Watch for theme changes
|
||||||
|
const observer = new MutationObserver(applyTheme);
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject CSS for dark mode styling
|
||||||
|
*/
|
||||||
|
injectDarkModeCSS() {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = this.styleId;
|
||||||
|
style.textContent = `
|
||||||
|
/* HubSpot Chat Widget Dark Mode Override */
|
||||||
|
#hubspot-conversations-inline-parent,
|
||||||
|
#hubspot-conversations-iframe-container,
|
||||||
|
.shadow-2xl.widget-align-right.widget-align-bottom,
|
||||||
|
[data-test-id="chat-widget"],
|
||||||
|
[class*="VizExCollapsedChat"],
|
||||||
|
[class*="VizExExpandedChat"],
|
||||||
|
iframe[src*="hubspot"] {
|
||||||
|
filter: invert(1) hue-rotate(180deg) !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Target HubSpot widget container backgrounds */
|
||||||
|
#hubspot-conversations-inline-parent div,
|
||||||
|
#hubspot-conversations-iframe-container div,
|
||||||
|
[data-test-id="chat-widget"] div {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent double inversion of images, avatars, and icons */
|
||||||
|
#hubspot-conversations-iframe-container img,
|
||||||
|
#hubspot-conversations-iframe-container [style*="background-image"],
|
||||||
|
#hubspot-conversations-iframe-container svg,
|
||||||
|
iframe[src*="hubspot"] img,
|
||||||
|
iframe[src*="hubspot"] svg,
|
||||||
|
[data-test-id="chat-widget"] img,
|
||||||
|
[data-test-id="chat-widget"] svg {
|
||||||
|
filter: invert(1) hue-rotate(180deg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional targeting for widget launcher and chat bubble */
|
||||||
|
div[class*="shadow-2xl"],
|
||||||
|
div[class*="widget-align"],
|
||||||
|
div[style*="position: fixed"] {
|
||||||
|
filter: invert(1) hue-rotate(180deg) !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove HubSpot widget and associated styles
|
||||||
|
*/
|
||||||
|
cleanup() {
|
||||||
|
const script = document.getElementById(this.scriptId);
|
||||||
|
const style = document.getElementById(this.styleId);
|
||||||
|
|
||||||
|
if (script) script.remove();
|
||||||
|
if (style) style.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize HubSpot integration
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const hubspot = new HubSpotManager();
|
||||||
|
hubspot.init();
|
||||||
|
|
||||||
|
// Make available globally for potential cleanup
|
||||||
|
window.HubSpotManager = hubspot;
|
||||||
|
});
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import { Col, ConfigProvider, Layout } from '@/shared/antd-imports';
|
import { Col, ConfigProvider, Layout } from '@/shared/antd-imports';
|
||||||
import { Outlet, useNavigate } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { memo, useMemo, useEffect, useRef } from 'react';
|
import { memo, useMemo, useEffect, useRef } from 'react';
|
||||||
import { useMediaQuery } from 'react-responsive';
|
import { useMediaQuery } from 'react-responsive';
|
||||||
|
|
||||||
import Navbar from '../features/navbar/navbar';
|
import Navbar from '../features/navbar/navbar';
|
||||||
import { useAppSelector } from '../hooks/useAppSelector';
|
import { useAppSelector } from '../hooks/useAppSelector';
|
||||||
import { useAppDispatch } from '../hooks/useAppDispatch';
|
|
||||||
import { colors } from '../styles/colors';
|
import { colors } from '../styles/colors';
|
||||||
|
|
||||||
import { useRenderPerformance } from '@/utils/performance';
|
import { useRenderPerformance } from '@/utils/performance';
|
||||||
import HubSpot from '@/components/HubSpot';
|
|
||||||
import { DynamicCSSLoader, LayoutStabilizer } from '@/utils/css-optimizations';
|
import { DynamicCSSLoader, LayoutStabilizer } from '@/utils/css-optimizations';
|
||||||
|
|
||||||
const MainLayout = memo(() => {
|
const MainLayout = memo(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user