From 4ad1120758bddf2b70777ff31863c41a45274eb9 Mon Sep 17 00:00:00 2001
From: Chamika J <75464293+chamikaJ@users.noreply.github.com>
Date: Tue, 12 Aug 2025 11:39:04 +0530
Subject: [PATCH 3/7] refactor(auth): normalize email handling for
case-insensitivity
- Updated email queries in authentication processes to use lowercase normalization for consistent case-insensitive comparisons.
- Adjusted relevant functions in `auth-controller`, `passport-local-login`, `passport-local-signup`, and frontend pages to ensure email is stored and compared in a uniform manner.
- Enhanced user experience by preventing issues related to email case discrepancies during login, signup, and password reset operations.
---
.../src/controllers/auth-controller.ts | 16 ++++++++++------
.../passport-strategies/passport-local-login.ts | 9 ++++++---
.../passport-strategies/passport-local-signup.ts | 10 +++++-----
.../src/pages/auth/ForgotPasswordPage.tsx | 4 +++-
worklenz-frontend/src/pages/auth/LoginPage.tsx | 8 +++++++-
worklenz-frontend/src/pages/auth/SignupPage.tsx | 2 +-
6 files changed, 32 insertions(+), 17 deletions(-)
diff --git a/worklenz-backend/src/controllers/auth-controller.ts b/worklenz-backend/src/controllers/auth-controller.ts
index 8ad790710..10176c487 100644
--- a/worklenz-backend/src/controllers/auth-controller.ts
+++ b/worklenz-backend/src/controllers/auth-controller.ts
@@ -114,8 +114,11 @@ export default class AuthController extends WorklenzControllerBase {
public static async reset_password(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const {email} = req.body;
- const q = `SELECT id, email, google_id, password FROM users WHERE email = $1;`;
- const result = await db.query(q, [email || null]);
+ // Normalize email to lowercase for case-insensitive comparison
+ const normalizedEmail = email ? email.toLowerCase().trim() : null;
+
+ const q = `SELECT id, email, google_id, password FROM users WHERE LOWER(email) = $1;`;
+ const result = await db.query(q, [normalizedEmail]);
if (!result.rowCount)
return res.status(200).send(new ServerResponse(false, null, "Account does not exists!"));
@@ -297,15 +300,16 @@ export default class AuthController extends WorklenzControllerBase {
}
// Check for existing local account
- const localAccountResult = await db.query("SELECT 1 FROM users WHERE email = $1 AND password IS NOT NULL AND is_deleted IS FALSE;", [profile.email]);
+ const normalizedProfileEmail = profile.email.toLowerCase().trim();
+ const localAccountResult = await db.query("SELECT 1 FROM users WHERE LOWER(email) = $1 AND password IS NOT NULL AND is_deleted IS FALSE;", [normalizedProfileEmail]);
if (localAccountResult.rowCount) {
return res.status(400).send(new ServerResponse(false, null, `No Google account exists for email ${profile.email}.`));
}
// Check if user exists
const userResult = await db.query(
- "SELECT id, google_id, name, email, active_team FROM users WHERE google_id = $1 OR email = $2;",
- [profile.sub, profile.email]
+ "SELECT id, google_id, name, email, active_team FROM users WHERE google_id = $1 OR LOWER(email) = $2;",
+ [profile.sub, normalizedProfileEmail]
);
let user: any;
@@ -317,7 +321,7 @@ export default class AuthController extends WorklenzControllerBase {
const googleUserData = {
id: profile.sub,
displayName: profile.name,
- email: profile.email,
+ email: normalizedProfileEmail,
picture: profile.picture
};
diff --git a/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts b/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts
index d71c4a366..4d64fbdcb 100644
--- a/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts
+++ b/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts
@@ -16,12 +16,15 @@ async function handleLogin(req: Request, email: string, password: string, done:
}
try {
+ // Normalize email to lowercase for case-insensitive comparison
+ const normalizedEmail = email.toLowerCase().trim();
+
const q = `SELECT id, email, google_id, password
FROM users
- WHERE email = $1
+ WHERE LOWER(email) = $1
AND google_id IS NULL
AND is_deleted IS FALSE;`;
- const result = await db.query(q, [email]);
+ const result = await db.query(q, [normalizedEmail]);
const [data] = result.rows;
@@ -33,7 +36,7 @@ async function handleLogin(req: Request, email: string, password: string, done:
const passwordMatch = bcrypt.compareSync(password, data.password);
- if (passwordMatch && email === data.email) {
+ if (passwordMatch) {
delete data.password;
const successMsg = "User successfully logged in";
req.flash(SUCCESS_KEY, successMsg);
diff --git a/worklenz-backend/src/passport/passport-strategies/passport-local-signup.ts b/worklenz-backend/src/passport/passport-strategies/passport-local-signup.ts
index fddad7f55..42b551f72 100644
--- a/worklenz-backend/src/passport/passport-strategies/passport-local-signup.ts
+++ b/worklenz-backend/src/passport/passport-strategies/passport-local-signup.ts
@@ -13,10 +13,10 @@ async function isGoogleAccountFound(email: string) {
const q = `
SELECT 1
FROM users
- WHERE email = $1
+ WHERE LOWER(email) = $1
AND google_id IS NOT NULL;
`;
- const result = await db.query(q, [email]);
+ const result = await db.query(q, [email.toLowerCase().trim()]);
return !!result.rowCount;
}
@@ -24,10 +24,10 @@ async function isAccountDeactivated(email: string) {
const q = `
SELECT 1
FROM users
- WHERE email = $1
+ WHERE LOWER(email) = $1
AND is_deleted = TRUE;
`;
- const result = await db.query(q, [email]);
+ const result = await db.query(q, [email.toLowerCase().trim()]);
return !!result.rowCount;
}
@@ -41,7 +41,7 @@ async function registerUser(password: string, team_id: string, name: string, tea
const body = {
name,
team_name,
- email,
+ email: email.toLowerCase().trim(),
password: encryptedPassword,
timezone,
invited_team_id: teamId,
diff --git a/worklenz-frontend/src/pages/auth/ForgotPasswordPage.tsx b/worklenz-frontend/src/pages/auth/ForgotPasswordPage.tsx
index da3e35636..e9c76ed86 100644
--- a/worklenz-frontend/src/pages/auth/ForgotPasswordPage.tsx
+++ b/worklenz-frontend/src/pages/auth/ForgotPasswordPage.tsx
@@ -64,7 +64,9 @@ const ForgotPasswordPage = () => {
if (values.email.trim() === '') return;
try {
setIsLoading(true);
- const result = await dispatch(resetPassword(values.email)).unwrap();
+ // Normalize email to lowercase for case-insensitive comparison
+ const normalizedEmail = values.email.toLowerCase().trim();
+ const result = await dispatch(resetPassword(normalizedEmail)).unwrap();
if (result.done) {
trackMixpanelEvent(evt_reset_password_click);
setIsSuccess(true);
diff --git a/worklenz-frontend/src/pages/auth/LoginPage.tsx b/worklenz-frontend/src/pages/auth/LoginPage.tsx
index 1f8b8824a..df4a4ab96 100644
--- a/worklenz-frontend/src/pages/auth/LoginPage.tsx
+++ b/worklenz-frontend/src/pages/auth/LoginPage.tsx
@@ -106,7 +106,13 @@ const LoginPage: React.FC = () => {
// localStorage.setItem(WORKLENZ_REDIRECT_PROJ_KEY, teamId);
// }
- const result = await dispatch(login(values)).unwrap();
+ // Normalize email to lowercase for case-insensitive comparison
+ const normalizedValues = {
+ ...values,
+ email: values.email.toLowerCase().trim()
+ };
+
+ const result = await dispatch(login(normalizedValues)).unwrap();
if (result.authenticated) {
message.success(t('successMessage'));
setSession(result.user);
diff --git a/worklenz-frontend/src/pages/auth/SignupPage.tsx b/worklenz-frontend/src/pages/auth/SignupPage.tsx
index 065989d23..fd3534802 100644
--- a/worklenz-frontend/src/pages/auth/SignupPage.tsx
+++ b/worklenz-frontend/src/pages/auth/SignupPage.tsx
@@ -215,7 +215,7 @@ const SignupPage = () => {
const body = {
name: values.name,
- email: values.email,
+ email: values.email.toLowerCase().trim(),
password: values.password,
};
From 2c01a98fbf599ad2169a54f7682a41076d5fb5b5 Mon Sep 17 00:00:00 2001
From: Chamika J <75464293+chamikaJ@users.noreply.github.com>
Date: Tue, 12 Aug 2025 12:11:43 +0530
Subject: [PATCH 4/7] feat(localization): add "or" string for team switching
across multiple languages
- Added the "or" string to localization files for Albanian, German, English, Spanish, Portuguese, and Chinese to enhance clarity in team switching instructions.
- Improved user experience by providing consistent language support for team switching functionality.
---
.../public/locales/alb/common.json | 1 +
.../public/locales/de/common.json | 1 +
.../public/locales/en/common.json | 1 +
.../public/locales/es/common.json | 1 +
.../public/locales/pt/common.json | 1 +
.../public/locales/zh/common.json | 1 +
.../LicenseExpiredModal.css | 19 ++
.../LicenseExpiredModal.tsx | 175 ++++++++++++------
.../src/hooks/useMixpanelTracking.tsx | 74 +++++---
worklenz-frontend/src/utils/mixpanelInit.ts | 7 +-
10 files changed, 204 insertions(+), 77 deletions(-)
diff --git a/worklenz-frontend/public/locales/alb/common.json b/worklenz-frontend/public/locales/alb/common.json
index eb79ddf86..edd27224e 100644
--- a/worklenz-frontend/public/locales/alb/common.json
+++ b/worklenz-frontend/public/locales/alb/common.json
@@ -37,6 +37,7 @@
"select-team": "Zgjidh skuadrën",
"owned-by": "Në pronësi të",
"switch-team-active-subscription": "Kaloni në një skuadër me një abonim aktiv për të vazhduar punën",
+ "or": "ose",
"license-expiring-soon": "Licenca juaj skadon në {{days}} ditë",
"license-expiring-soon_plural": "Licenca juaj skadon në {{days}} ditë",
"license-expiring-today": "Licenca juaj skadon sot!",
diff --git a/worklenz-frontend/public/locales/de/common.json b/worklenz-frontend/public/locales/de/common.json
index b4cea9fd8..79acf761e 100644
--- a/worklenz-frontend/public/locales/de/common.json
+++ b/worklenz-frontend/public/locales/de/common.json
@@ -47,6 +47,7 @@
"select-team": "Team auswählen",
"owned-by": "Gehört",
"switch-team-active-subscription": "Wechseln Sie zu einem Team mit einem aktiven Abonnement, um weiterzuarbeiten",
+ "or": "oder",
"license-expiring-soon": "Ihre Lizenz läuft in {{days}} Tag ab",
"license-expiring-soon_plural": "Ihre Lizenz läuft in {{days}} Tagen ab",
"license-expiring-today": "Ihre Lizenz läuft heute ab!",
diff --git a/worklenz-frontend/public/locales/en/common.json b/worklenz-frontend/public/locales/en/common.json
index 0ee6c2672..e18b0a7be 100644
--- a/worklenz-frontend/public/locales/en/common.json
+++ b/worklenz-frontend/public/locales/en/common.json
@@ -47,6 +47,7 @@
"select-team": "Select Team",
"owned-by": "Owned by",
"switch-team-active-subscription": "Switch to a team with an active subscription to continue working",
+ "or": "or",
"license-expiring-soon": "Your license expires in {{days}} day",
"license-expiring-soon_plural": "Your license expires in {{days}} days",
"license-expiring-today": "Your license expires today!",
diff --git a/worklenz-frontend/public/locales/es/common.json b/worklenz-frontend/public/locales/es/common.json
index d5b541f9b..54f548603 100644
--- a/worklenz-frontend/public/locales/es/common.json
+++ b/worklenz-frontend/public/locales/es/common.json
@@ -47,6 +47,7 @@
"select-team": "Seleccionar equipo",
"owned-by": "Propiedad de",
"switch-team-active-subscription": "Cambie a un equipo con una suscripción activa para continuar trabajando",
+ "or": "o",
"license-expiring-soon": "Su licencia expira en {{days}} día",
"license-expiring-soon_plural": "Su licencia expira en {{days}} días",
"license-expiring-today": "¡Su licencia expira hoy!",
diff --git a/worklenz-frontend/public/locales/pt/common.json b/worklenz-frontend/public/locales/pt/common.json
index fec5a3296..997551287 100644
--- a/worklenz-frontend/public/locales/pt/common.json
+++ b/worklenz-frontend/public/locales/pt/common.json
@@ -37,6 +37,7 @@
"select-team": "Selecionar equipe",
"owned-by": "Propriedade de",
"switch-team-active-subscription": "Mude para uma equipe com uma assinatura ativa para continuar trabalhando",
+ "or": "ou",
"license-expiring-soon": "Sua licença expira em {{days}} dia",
"license-expiring-soon_plural": "Sua licença expira em {{days}} dias",
"license-expiring-today": "Sua licença expira hoje!",
diff --git a/worklenz-frontend/public/locales/zh/common.json b/worklenz-frontend/public/locales/zh/common.json
index d15007953..dc88d3833 100644
--- a/worklenz-frontend/public/locales/zh/common.json
+++ b/worklenz-frontend/public/locales/zh/common.json
@@ -37,6 +37,7 @@
"select-team": "选择团队",
"owned-by": "拥有者",
"switch-team-active-subscription": "切换到有有效订阅的团队以继续工作",
+ "or": "或",
"license-expiring-soon": "您的许可证还有 {{days}} 天到期",
"license-expiring-soon_plural": "您的许可证还有 {{days}} 天到期",
"license-expiring-today": "您的许可证今天到期!",
diff --git a/worklenz-frontend/src/components/LicenseExpiredModal/LicenseExpiredModal.css b/worklenz-frontend/src/components/LicenseExpiredModal/LicenseExpiredModal.css
index 557ede03d..9bd988bfc 100644
--- a/worklenz-frontend/src/components/LicenseExpiredModal/LicenseExpiredModal.css
+++ b/worklenz-frontend/src/components/LicenseExpiredModal/LicenseExpiredModal.css
@@ -11,6 +11,25 @@
z-index: 1060 !important;
}
+/* Theme-aware dropdown styling */
+.switch-team-dropdown .ant-dropdown-menu {
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+[data-theme="dark"] .switch-team-dropdown .ant-dropdown-menu {
+ background-color: #262626;
+ border: 1px solid #434343;
+}
+
+[data-theme="dark"] .switch-team-dropdown .ant-dropdown-menu-item {
+ color: #fff;
+}
+
+[data-theme="dark"] .switch-team-dropdown .ant-dropdown-menu-item:hover {
+ background-color: #303030;
+}
+
/* Ensure the modal content is properly styled */
.license-expired-modal-wrap .ant-modal-content {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
diff --git a/worklenz-frontend/src/components/LicenseExpiredModal/LicenseExpiredModal.tsx b/worklenz-frontend/src/components/LicenseExpiredModal/LicenseExpiredModal.tsx
index 53c4e0d34..fb56ee25c 100644
--- a/worklenz-frontend/src/components/LicenseExpiredModal/LicenseExpiredModal.tsx
+++ b/worklenz-frontend/src/components/LicenseExpiredModal/LicenseExpiredModal.tsx
@@ -36,6 +36,7 @@ export const LicenseExpiredModal = ({ open, subscriptionType = ISUBSCRIPTION_TYP
// Team switching state
const teamsList = useAppSelector(state => state.teamReducer.teamsList);
const session = authService?.getCurrentSession();
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
useEffect(() => {
setVisible(open);
@@ -76,27 +77,43 @@ export const LicenseExpiredModal = ({ open, subscriptionType = ISUBSCRIPTION_TYP
className="switch-team-card"
onClick={() => handleTeamSelect(team.id)}
bordered={false}
- style={{ width: 230, cursor: 'pointer' }}
+ style={{
+ width: '100%',
+ cursor: 'pointer',
+ backgroundColor: themeMode === 'dark' ? '#262626' : '#fff',
+ color: themeMode === 'dark' ? '#fff' : '#000'
+ }}
>
-
+
{t('owned-by')} {team.owns_by}
- {team.name}
+
+ {team.name}
+
- {index < teamsList.length - 1 && }
+ {index < teamsList.length - 1 && }
);
@@ -218,56 +235,6 @@ export const LicenseExpiredModal = ({ open, subscriptionType = ISUBSCRIPTION_TYP
>
- {/* Team Switcher - Show prominently if multiple teams exist */}
- {teamsList && teamsList.length > 1 && (
-
-
-
- {t('switch-team-to-continue')}
-
-
-
-
-
- {t('switch-team-active-subscription')}
-
-
-
- )}
{/* Icon and Title */}
@@ -334,6 +301,104 @@ export const LicenseExpiredModal = ({ open, subscriptionType = ISUBSCRIPTION_TYP
}
+ {/* Team Switcher - Show below upgrade button if multiple teams exist */}
+ {teamsList && teamsList.length > 1 && (
+ <>
+
+
+
+
+ {t('switch-team-to-continue')}
+
+
+
+
+
+ {t('switch-team-active-subscription')}
+
+
+
+ >
+ )}
+
{/* Note */}
Note
diff --git a/worklenz-frontend/src/hooks/useMixpanelTracking.tsx b/worklenz-frontend/src/hooks/useMixpanelTracking.tsx
index d5cd64ca2..408cb705a 100644
--- a/worklenz-frontend/src/hooks/useMixpanelTracking.tsx
+++ b/worklenz-frontend/src/hooks/useMixpanelTracking.tsx
@@ -7,39 +7,70 @@ import logger from '@/utils/errorLogger';
export const useMixpanelTracking = () => {
const auth = useAuthService();
- const token = useMemo(() => {
+ const { token, isProductionEnvironment } = useMemo(() => {
const host = window.location.host;
- if (host === 'uat.worklenz.com' || host === 'dev.worklenz.com' || host === 'api.worklenz.com') {
- return import.meta.env.VITE_MIXPANEL_TOKEN;
- }
- if (host === 'app.worklenz.com' || host === 'v2.worklenz.com') {
- return import.meta.env.VITE_MIXPANEL_TOKEN;
- }
- return import.meta.env.VITE_MIXPANEL_TOKEN;
+ const isProduction = host === 'app.worklenz.com';
+
+ return {
+ token: isProduction ? import.meta.env.VITE_MIXPANEL_TOKEN : null,
+ isProductionEnvironment: isProduction
+ };
}, []);
useEffect(() => {
- initMixpanel(token);
- }, [token]);
+ if (isProductionEnvironment && token) {
+ try {
+ initMixpanel(token);
+ logger.info('Mixpanel initialized successfully for production');
+ } catch (error) {
+ logger.error('Failed to initialize Mixpanel:', error);
+ }
+ } else {
+ logger.info('Mixpanel not initialized - not in production environment or missing token');
+ }
+ }, [token, isProductionEnvironment]);
const setIdentity = useCallback((user: any) => {
+ if (!isProductionEnvironment) {
+ logger.debug('Mixpanel setIdentity skipped - not in production environment');
+ return;
+ }
+
if (user?.id) {
- mixpanel.identify(user.id);
- mixpanel.people.set({
- $user_id: user.id,
- $name: user.name,
- $email: user.email,
- $avatar: user.avatar_url,
- });
+ try {
+ mixpanel.identify(user.id);
+ mixpanel.people.set({
+ $user_id: user.id,
+ $name: user.name,
+ $email: user.email,
+ $avatar: user.avatar_url,
+ });
+ } catch (error) {
+ logger.error('Error setting Mixpanel identity:', error);
+ }
}
- }, []);
+ }, [isProductionEnvironment]);
const reset = useCallback(() => {
- mixpanel.reset();
- }, []);
+ if (!isProductionEnvironment) {
+ logger.debug('Mixpanel reset skipped - not in production environment');
+ return;
+ }
+
+ try {
+ mixpanel.reset();
+ } catch (error) {
+ logger.error('Error resetting Mixpanel:', error);
+ }
+ }, [isProductionEnvironment]);
const trackMixpanelEvent = useCallback(
(event: string, properties?: Dict) => {
+ if (!isProductionEnvironment) {
+ logger.debug(`Mixpanel tracking skipped - not in production environment. Event: ${event}`, properties);
+ return;
+ }
+
try {
const currentUser = auth.getCurrentSession();
const props = {
@@ -48,11 +79,12 @@ export const useMixpanelTracking = () => {
};
mixpanel.track(event, props);
+ logger.debug(`Mixpanel event tracked: ${event}`, props);
} catch (e) {
logger.error('Error tracking mixpanel event', e);
}
},
- [auth.getCurrentSession]
+ [auth.getCurrentSession, isProductionEnvironment]
);
return {
diff --git a/worklenz-frontend/src/utils/mixpanelInit.ts b/worklenz-frontend/src/utils/mixpanelInit.ts
index 809a176e7..b27f2d7fd 100644
--- a/worklenz-frontend/src/utils/mixpanelInit.ts
+++ b/worklenz-frontend/src/utils/mixpanelInit.ts
@@ -1,7 +1,12 @@
import { MixpanelConfig } from '@/types/mixpanel.types';
import mixpanel from 'mixpanel-browser';
-export const initMixpanel = (token: string, config: MixpanelConfig = {}): void => {
+export const initMixpanel = (token: string | null, config: MixpanelConfig = {}): void => {
+ if (!token || token === 'mixpanel-token' || token.trim() === '') {
+ console.warn('Mixpanel initialization skipped: Invalid or missing token');
+ return;
+ }
+
mixpanel.init(token, {
debug: import.meta.env.VITE_APP_ENV !== 'production',
track_pageview: true,
From 3caf36e990483689e4ad0f6064f44106f3d9bb8b Mon Sep 17 00:00:00 2001
From: Chamika J <75464293+chamikaJ@users.noreply.github.com>
Date: Tue, 12 Aug 2025 12:32:51 +0530
Subject: [PATCH 5/7] feat(analytics): integrate Mixpanel tracking across
various components
- Added Mixpanel tracking events for user interactions in components such as CreateProjectButton, CreateTaskModal, Navbar, and various admin center pages.
- Enhanced user experience by capturing analytics for actions like project creation, task creation, team switching, and page visits.
- Improved tracking for settings and reporting pages to better understand user engagement and behavior.
---
.../project-create-button/project-create-button.tsx | 3 ++-
.../src/components/task-management/CreateTaskModal.tsx | 6 ++++++
worklenz-frontend/src/features/navbar/navbar.tsx | 3 +++
.../features/navbar/switch-team/SwitchTeamButton.tsx | 5 +++++
.../src/pages/admin-center/overview/overview.tsx | 6 +++++-
.../src/pages/admin-center/projects/projects.tsx | 7 +++++++
.../src/pages/admin-center/teams/teams.tsx | 8 ++++++++
.../src/pages/admin-center/users/users.tsx | 7 +++++++
worklenz-frontend/src/pages/auth/LoggingOutPage.tsx | 9 +++++++++
.../project-view-1/roadmap/project-view-roadmap.tsx | 9 ++++++++-
.../project-view-1/roadmap/roadmap-grant-chart.tsx | 4 ++++
.../project-view-1/workload/ProjectViewWorkload.tsx | 10 +++++++++-
.../reporting/members-reports/members-reports.tsx | 7 +++++++
.../reporting/projects-reports/projects-reports.tsx | 9 ++++++++-
worklenz-frontend/src/pages/schedule/schedule.tsx | 9 ++++++++-
.../pages/settings/categories/categories-settings.tsx | 7 +++++++
.../src/pages/settings/clients/client-drawer.tsx | 4 ++++
.../src/pages/settings/clients/clients-settings.tsx | 7 +++++++
.../pages/settings/job-titles/job-titles-drawer.tsx | 4 ++++
.../pages/settings/job-titles/job-titles-settings.tsx | 7 +++++++
.../src/pages/settings/labels/LabelsSettings.tsx | 7 +++++++
.../settings/notifications/notifications-settings.tsx | 6 +++++-
.../task-templates/task-templates-settings.tsx | 6 +++++-
.../src/pages/settings/teams/teams-settings.tsx | 6 +++++-
24 files changed, 147 insertions(+), 9 deletions(-)
diff --git a/worklenz-frontend/src/components/projects/project-create-button/project-create-button.tsx b/worklenz-frontend/src/components/projects/project-create-button/project-create-button.tsx
index 2cb2729b2..795e79215 100644
--- a/worklenz-frontend/src/components/projects/project-create-button/project-create-button.tsx
+++ b/worklenz-frontend/src/components/projects/project-create-button/project-create-button.tsx
@@ -12,7 +12,7 @@ import {
} from '@/features/project/project-drawer.slice';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import { projectTemplatesApiService } from '@/api/project-templates/project-templates.api.service';
-import { evt_projects_create_click } from '@/shared/worklenz-analytics-events';
+import { evt_projects_create_click, evt_project_import_from_template_click } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
interface CreateProjectButtonProps {
className?: string;
@@ -36,6 +36,7 @@ const CreateProjectButton: React.FC = ({ className })
}, [location]);
const handleTemplateDrawerOpen = () => {
+ trackMixpanelEvent(evt_project_import_from_template_click);
setIsTemplateDrawerOpen(true);
};
diff --git a/worklenz-frontend/src/components/task-management/CreateTaskModal.tsx b/worklenz-frontend/src/components/task-management/CreateTaskModal.tsx
index 54d3ad16a..cc81ee08d 100644
--- a/worklenz-frontend/src/components/task-management/CreateTaskModal.tsx
+++ b/worklenz-frontend/src/components/task-management/CreateTaskModal.tsx
@@ -19,6 +19,8 @@ import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { useAuthService } from '@/hooks/useAuth';
import { fetchTasksV3 } from '@/features/task-management/task-management.slice';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_project_task_create } from '@/shared/worklenz-analytics-events';
import './CreateTaskModal.css';
const { Title, Text } = Typography;
@@ -351,6 +353,7 @@ const CreateTaskModal: React.FC = ({
const [form] = Form.useForm();
const [activeTab, setActiveTab] = useState('task-info');
const dispatch = useAppDispatch();
+ const { trackMixpanelEvent } = useMixpanelTracking();
// Redux state
const isDarkMode = useAppSelector(state => state.themeReducer?.mode === 'dark');
@@ -381,6 +384,9 @@ const CreateTaskModal: React.FC = ({
reporter_id: user.id,
};
+ // Track analytics event
+ trackMixpanelEvent(evt_project_task_create);
+
// Create task via socket
socket.emit(SocketEvents.QUICK_TASK.toString(), taskData);
diff --git a/worklenz-frontend/src/features/navbar/navbar.tsx b/worklenz-frontend/src/features/navbar/navbar.tsx
index 71f6fe2fb..cac66ec45 100644
--- a/worklenz-frontend/src/features/navbar/navbar.tsx
+++ b/worklenz-frontend/src/features/navbar/navbar.tsx
@@ -22,6 +22,7 @@ import { authApiService } from '@/api/auth/auth.api.service';
import { ISUBSCRIPTION_TYPE } from '@/shared/constants';
import logger from '@/utils/errorLogger';
import TimerButton from './timers/TimerButton';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const Navbar = () => {
const [current, setCurrent] = useState('home');
@@ -32,6 +33,7 @@ const Navbar = () => {
const { isDesktop, isMobile, isTablet } = useResponsive();
const { t } = useTranslation('navbar');
const authService = useAuthService();
+ const { setIdentity } = useMixpanelTracking();
const [navRoutesList, setNavRoutesList] = useState(navRoutes);
const [isOwnerOrAdmin, setIsOwnerOrAdmin] = useState(authService.isOwnerOrAdmin());
const showUpgradeTypes = [
@@ -44,6 +46,7 @@ const Navbar = () => {
.then(authorizeResponse => {
if (authorizeResponse.authenticated) {
authService.setCurrentSession(authorizeResponse.user);
+ setIdentity(authorizeResponse.user);
setIsOwnerOrAdmin(!!(authorizeResponse.user.is_admin || authorizeResponse.user.owner));
}
})
diff --git a/worklenz-frontend/src/features/navbar/switch-team/SwitchTeamButton.tsx b/worklenz-frontend/src/features/navbar/switch-team/SwitchTeamButton.tsx
index b2dddbc40..7f28afde0 100644
--- a/worklenz-frontend/src/features/navbar/switch-team/SwitchTeamButton.tsx
+++ b/worklenz-frontend/src/features/navbar/switch-team/SwitchTeamButton.tsx
@@ -18,6 +18,8 @@ import { useAuthService } from '@/hooks/useAuth';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { createAuthService } from '@/services/auth/auth.service';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_common_switch_team } from '@/shared/worklenz-analytics-events';
// Components
import CustomAvatar from '@/components/CustomAvatar';
@@ -34,6 +36,7 @@ const SwitchTeamButton = () => {
const { getCurrentSession } = useAuthService();
const session = getCurrentSession();
const { t } = useTranslation('navbar');
+ const { setIdentity, trackMixpanelEvent } = useMixpanelTracking();
// Selectors
const teamsList = useAppSelector(state => state.teamReducer.teamsList);
@@ -53,12 +56,14 @@ const SwitchTeamButton = () => {
if (result.authenticated) {
dispatch(setUser(result.user));
authService.setCurrentSession(result.user);
+ setIdentity(result.user);
}
};
const handleTeamSelect = async (id: string) => {
if (!id) return;
+ trackMixpanelEvent(evt_common_switch_team);
await dispatch(setActiveTeam(id));
await handleVerifyAuth();
window.location.reload();
diff --git a/worklenz-frontend/src/pages/admin-center/overview/overview.tsx b/worklenz-frontend/src/pages/admin-center/overview/overview.tsx
index dd46d6057..5c2c9f9cc 100644
--- a/worklenz-frontend/src/pages/admin-center/overview/overview.tsx
+++ b/worklenz-frontend/src/pages/admin-center/overview/overview.tsx
@@ -12,6 +12,8 @@ import { adminCenterApiService } from '@/api/admin-center/admin-center.api.servi
import { IOrganization, IOrganizationAdmin } from '@/types/admin-center/admin-center.types';
import logger from '@/utils/errorLogger';
import { tr } from 'date-fns/locale';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_admin_center_overview_visit } from '@/shared/worklenz-analytics-events';
const { Text } = Typography;
@@ -19,6 +21,7 @@ const Overview: React.FC = () => {
const [organization, setOrganization] = useState(null);
const [organizationAdmins, setOrganizationAdmins] = useState(null);
const [loadingAdmins, setLoadingAdmins] = useState(false);
+ const { trackMixpanelEvent } = useMixpanelTracking();
const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode);
const { t } = useTranslation('admin-center/overview');
@@ -49,9 +52,10 @@ const Overview: React.FC = () => {
};
useEffect(() => {
+ trackMixpanelEvent(evt_admin_center_overview_visit);
getOrganizationDetails();
getOrganizationAdmins();
- }, []);
+ }, [trackMixpanelEvent]);
return (
diff --git a/worklenz-frontend/src/pages/admin-center/projects/projects.tsx b/worklenz-frontend/src/pages/admin-center/projects/projects.tsx
index 46eaf2149..d10a1d40e 100644
--- a/worklenz-frontend/src/pages/admin-center/projects/projects.tsx
+++ b/worklenz-frontend/src/pages/admin-center/projects/projects.tsx
@@ -11,6 +11,8 @@ import { formatDateTimeWithLocale } from '@/utils/format-date-time-with-locale';
import logger from '@/utils/errorLogger';
import { deleteProject } from '@features/projects/projectsSlice';
import './projects.css';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_admin_center_projects_visit } from '@/shared/worklenz-analytics-events';
import {
Button,
Card,
@@ -39,6 +41,7 @@ const Projects: React.FC = () => {
order: 'desc',
search: '',
});
+ const { trackMixpanelEvent } = useMixpanelTracking();
const dispatch = useAppDispatch();
@@ -70,6 +73,10 @@ const Projects: React.FC = () => {
}
};
+ useEffect(() => {
+ trackMixpanelEvent(evt_admin_center_projects_visit);
+ }, [trackMixpanelEvent]);
+
useEffect(() => {
fetchProjects();
}, [
diff --git a/worklenz-frontend/src/pages/admin-center/teams/teams.tsx b/worklenz-frontend/src/pages/admin-center/teams/teams.tsx
index f5043392e..d5b317344 100644
--- a/worklenz-frontend/src/pages/admin-center/teams/teams.tsx
+++ b/worklenz-frontend/src/pages/admin-center/teams/teams.tsx
@@ -20,6 +20,8 @@ import logger from '@/utils/errorLogger';
import { RootState } from '@/app/store';
import { useTranslation } from 'react-i18next';
import AddTeamDrawer from '@/components/admin-center/teams/add-team-drawer/add-team-drawer';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_admin_center_teams_visit } from '@/shared/worklenz-analytics-events';
export interface IRequestParams extends IOrganizationTeamRequestParams {
total: number;
@@ -28,6 +30,7 @@ export interface IRequestParams extends IOrganizationTeamRequestParams {
const Teams: React.FC = () => {
const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode);
const { t } = useTranslation('admin-center/teams');
+ const { trackMixpanelEvent } = useMixpanelTracking();
const [showAddTeamDrawer, setShowAddTeamDrawer] = useState(false);
@@ -64,6 +67,11 @@ const Teams: React.FC = () => {
}
};
+ useEffect(() => {
+ trackMixpanelEvent(evt_admin_center_teams_visit);
+ fetchTeams();
+ }, [trackMixpanelEvent]);
+
useEffect(() => {
fetchTeams();
}, [requestParams.search]);
diff --git a/worklenz-frontend/src/pages/admin-center/users/users.tsx b/worklenz-frontend/src/pages/admin-center/users/users.tsx
index d8db586cf..cce10eaef 100644
--- a/worklenz-frontend/src/pages/admin-center/users/users.tsx
+++ b/worklenz-frontend/src/pages/admin-center/users/users.tsx
@@ -11,9 +11,12 @@ import { DEFAULT_PAGE_SIZE, PAGE_SIZE_OPTIONS } from '@/shared/constants';
import logger from '@/utils/errorLogger';
import { formatDateTimeWithLocale } from '@/utils/format-date-time-with-locale';
import SingleAvatar from '@/components/common/single-avatar/single-avatar';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_admin_center_users_visit } from '@/shared/worklenz-analytics-events';
const Users: React.FC = () => {
const { t } = useTranslation('admin-center/users');
+ const { trackMixpanelEvent } = useMixpanelTracking();
const [isLoading, setIsLoading] = useState(false);
const [users, setUsers] = useState
([]);
@@ -73,6 +76,10 @@ const Users: React.FC = () => {
},
];
+ useEffect(() => {
+ trackMixpanelEvent(evt_admin_center_users_visit);
+ }, [trackMixpanelEvent]);
+
useEffect(() => {
fetchUsers();
}, [requestParams.searchTerm, requestParams.page, requestParams.pageSize]);
diff --git a/worklenz-frontend/src/pages/auth/LoggingOutPage.tsx b/worklenz-frontend/src/pages/auth/LoggingOutPage.tsx
index c5e94c25b..1d6fa3292 100644
--- a/worklenz-frontend/src/pages/auth/LoggingOutPage.tsx
+++ b/worklenz-frontend/src/pages/auth/LoggingOutPage.tsx
@@ -6,16 +6,25 @@ import { useAuthService } from '@/hooks/useAuth';
import { useMediaQuery } from 'react-responsive';
import { authApiService } from '@/api/auth/auth.api.service';
import CacheCleanup from '@/utils/cache-cleanup';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_common_logout } from '@/shared/worklenz-analytics-events';
const LoggingOutPage = () => {
const navigate = useNavigate();
const auth = useAuthService();
const { t } = useTranslation('auth/auth-common');
const isMobile = useMediaQuery({ query: '(max-width: 576px)' });
+ const { reset, trackMixpanelEvent } = useMixpanelTracking();
useEffect(() => {
const logout = async () => {
try {
+ // Track logout event
+ trackMixpanelEvent(evt_common_logout);
+
+ // Reset Mixpanel identity
+ reset();
+
// Clear local session
await auth.signOut();
diff --git a/worklenz-frontend/src/pages/projects/project-view-1/roadmap/project-view-roadmap.tsx b/worklenz-frontend/src/pages/projects/project-view-1/roadmap/project-view-roadmap.tsx
index cb7ca9141..7c8f53f50 100644
--- a/worklenz-frontend/src/pages/projects/project-view-1/roadmap/project-view-roadmap.tsx
+++ b/worklenz-frontend/src/pages/projects/project-view-1/roadmap/project-view-roadmap.tsx
@@ -1,4 +1,6 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
+import { useMixpanelTracking } from '../../../../hooks/useMixpanelTracking';
+import { evt_project_roadmap_visit } from '../../../../shared/worklenz-analytics-events';
import { ViewMode } from 'gantt-task-react';
import 'gantt-task-react/dist/index.css';
import './project-view-roadmap.css';
@@ -10,10 +12,15 @@ import RoadmapGrantChart from './roadmap-grant-chart';
const ProjectViewRoadmap = () => {
const [view, setView] = useState(ViewMode.Day);
+ const { trackMixpanelEvent } = useMixpanelTracking();
// get theme details
const themeMode = useAppSelector(state => state.themeReducer.mode);
+ useEffect(() => {
+ trackMixpanelEvent(evt_project_roadmap_visit);
+ }, [trackMixpanelEvent]);
+
return (
{/* time filter */}
diff --git a/worklenz-frontend/src/pages/projects/project-view-1/roadmap/roadmap-grant-chart.tsx b/worklenz-frontend/src/pages/projects/project-view-1/roadmap/roadmap-grant-chart.tsx
index 067d07231..2e2ce14ee 100644
--- a/worklenz-frontend/src/pages/projects/project-view-1/roadmap/roadmap-grant-chart.tsx
+++ b/worklenz-frontend/src/pages/projects/project-view-1/roadmap/roadmap-grant-chart.tsx
@@ -1,6 +1,8 @@
import { Gantt, Task, ViewMode } from 'gantt-task-react';
import React from 'react';
import { colors } from '../../../../styles/colors';
+import { useMixpanelTracking } from '../../../../hooks/useMixpanelTracking';
+import { evt_roadmap_drag_change_date, evt_roadmap_drag_move } from '../../../../shared/worklenz-analytics-events';
import {
NewTaskType,
updateTaskDate,
@@ -17,6 +19,7 @@ type RoadmapGrantChartProps = {
const RoadmapGrantChart = ({ view }: RoadmapGrantChartProps) => {
// get task list from roadmap slice
const tasks = useAppSelector(state => state.roadmapReducer.tasksList);
+ const { trackMixpanelEvent } = useMixpanelTracking();
const dispatch = useAppDispatch();
@@ -37,6 +40,7 @@ const RoadmapGrantChart = ({ view }: RoadmapGrantChartProps) => {
// function to handle date change
const handleTaskDateChange = (task: Task) => {
+ trackMixpanelEvent(evt_roadmap_drag_change_date);
dispatch(updateTaskDate({ taskId: task.id, start: task.start, end: task.end }));
};
diff --git a/worklenz-frontend/src/pages/projects/project-view-1/workload/ProjectViewWorkload.tsx b/worklenz-frontend/src/pages/projects/project-view-1/workload/ProjectViewWorkload.tsx
index c36ea1483..b0d8adaf3 100644
--- a/worklenz-frontend/src/pages/projects/project-view-1/workload/ProjectViewWorkload.tsx
+++ b/worklenz-frontend/src/pages/projects/project-view-1/workload/ProjectViewWorkload.tsx
@@ -1,6 +1,14 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_project_workload_visit } from '@/shared/worklenz-analytics-events';
const ProjectViewWorkload = () => {
+ const { trackMixpanelEvent } = useMixpanelTracking();
+
+ useEffect(() => {
+ trackMixpanelEvent(evt_project_workload_visit);
+ }, [trackMixpanelEvent]);
+
return ProjectViewWorkload
;
};
diff --git a/worklenz-frontend/src/pages/reporting/members-reports/members-reports.tsx b/worklenz-frontend/src/pages/reporting/members-reports/members-reports.tsx
index 192f7dae8..6e2f306e7 100644
--- a/worklenz-frontend/src/pages/reporting/members-reports/members-reports.tsx
+++ b/worklenz-frontend/src/pages/reporting/members-reports/members-reports.tsx
@@ -18,12 +18,15 @@ import {
import { useAuthService } from '@/hooks/useAuth';
import { reportingExportApiService } from '@/api/reporting/reporting-export.api.service';
import { useEffect } from 'react';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_reporting_allocation } from '@/shared/worklenz-analytics-events';
const MembersReports = () => {
const { t } = useTranslation('reporting-members');
const dispatch = useAppDispatch();
useDocumentTitle('Reporting - Members');
const currentSession = useAuthService().getCurrentSession();
+ const { trackMixpanelEvent } = useMixpanelTracking();
const { archived, searchQuery, total } = useAppSelector(state => state.membersReportsReducer);
const { duration, dateRange } = useAppSelector(state => state.reportingReducer);
@@ -38,6 +41,10 @@ const MembersReports = () => {
);
};
+ useEffect(() => {
+ trackMixpanelEvent(evt_reporting_allocation);
+ }, [trackMixpanelEvent]);
+
useEffect(() => {
dispatch(setDuration(duration));
dispatch(setDateRange(dateRange));
diff --git a/worklenz-frontend/src/pages/reporting/projects-reports/projects-reports.tsx b/worklenz-frontend/src/pages/reporting/projects-reports/projects-reports.tsx
index 028e7bc1b..275f5590b 100644
--- a/worklenz-frontend/src/pages/reporting/projects-reports/projects-reports.tsx
+++ b/worklenz-frontend/src/pages/reporting/projects-reports/projects-reports.tsx
@@ -1,5 +1,7 @@
import { Button, Card, Checkbox, Dropdown, Flex, Space, Typography } from '@/shared/antd-imports';
-import { useMemo, useCallback, memo } from 'react';
+import { useMemo, useCallback, memo, useEffect } from 'react';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_reporting_projects_overview } from '@/shared/worklenz-analytics-events';
import CustomPageHeader from '@/pages/reporting/page-header/custom-page-header';
import { DownOutlined } from '@/shared/antd-imports';
import ProjectReportsTable from './projects-reports-table/projects-reports-table';
@@ -16,11 +18,16 @@ const ProjectsReports = () => {
const { t } = useTranslation('reporting-projects');
const dispatch = useAppDispatch();
const currentSession = useAuthService().getCurrentSession();
+ const { trackMixpanelEvent } = useMixpanelTracking();
useDocumentTitle('Reporting - Projects');
const { total, archived } = useAppSelector(state => state.projectReportsReducer);
+ useEffect(() => {
+ trackMixpanelEvent(evt_reporting_projects_overview);
+ }, [trackMixpanelEvent]);
+
// Memoize the title to prevent recalculation on every render
const pageTitle = useMemo(() => {
return `${total === 1 ? `${total} ${t('projectCount')}` : `${total} ${t('projectCountPlural')}`} `;
diff --git a/worklenz-frontend/src/pages/schedule/schedule.tsx b/worklenz-frontend/src/pages/schedule/schedule.tsx
index aefe321ea..f78d9afb9 100644
--- a/worklenz-frontend/src/pages/schedule/schedule.tsx
+++ b/worklenz-frontend/src/pages/schedule/schedule.tsx
@@ -1,5 +1,7 @@
import { Button, DatePicker, DatePickerProps, Flex, Select, Space } from '@/shared/antd-imports';
-import React, { useRef } from 'react';
+import React, { useRef, useEffect } from 'react';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_schedule_page_visit } from '@/shared/worklenz-analytics-events';
import { SettingOutlined } from '@ant-design/icons';
import { useDispatch } from 'react-redux';
import { setDate, setType, toggleSettingsDrawer } from '@/features/schedule/scheduleSlice';
@@ -31,9 +33,14 @@ const Schedule: React.FC = () => {
const dispatch = useDispatch();
const granttChartRef = useRef(null);
const { date, type } = useAppSelector(state => state.scheduleReducer);
+ const { trackMixpanelEvent } = useMixpanelTracking();
useDocumentTitle('Schedule');
+ useEffect(() => {
+ trackMixpanelEvent(evt_schedule_page_visit);
+ }, [trackMixpanelEvent]);
+
const handleDateChange = (value: dayjs.Dayjs | null) => {
if (!value) return;
let selectedDate = value.toDate();
diff --git a/worklenz-frontend/src/pages/settings/categories/categories-settings.tsx b/worklenz-frontend/src/pages/settings/categories/categories-settings.tsx
index 2f7b5362e..7c52d158f 100644
--- a/worklenz-frontend/src/pages/settings/categories/categories-settings.tsx
+++ b/worklenz-frontend/src/pages/settings/categories/categories-settings.tsx
@@ -19,10 +19,13 @@ import { categoriesApiService } from '@/api/settings/categories/categories.api.s
import { IProjectCategory, IProjectCategoryViewModel } from '@/types/project/projectCategory.types';
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import { useAppDispatch } from '@/hooks/useAppDispatch';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_settings_categories_visit } from '@/shared/worklenz-analytics-events';
const CategoriesSettings = () => {
// localization
const { t } = useTranslation('settings/categories');
+ const { trackMixpanelEvent } = useMixpanelTracking();
useDocumentTitle('Manage Categories');
@@ -55,6 +58,10 @@ const CategoriesSettings = () => {
};
}, []);
+ useEffect(() => {
+ trackMixpanelEvent(evt_settings_categories_visit);
+ }, [trackMixpanelEvent]);
+
useEffect(() => {
getCategories();
}, [getCategories]);
diff --git a/worklenz-frontend/src/pages/settings/clients/client-drawer.tsx b/worklenz-frontend/src/pages/settings/clients/client-drawer.tsx
index d27f16b2d..0e62f511b 100644
--- a/worklenz-frontend/src/pages/settings/clients/client-drawer.tsx
+++ b/worklenz-frontend/src/pages/settings/clients/client-drawer.tsx
@@ -9,6 +9,8 @@ import {
} from '@/features/settings/client/clientSlice';
import { IClient } from '@/types/client.types';
import { useTranslation } from 'react-i18next';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_settings_clients_create } from '@/shared/worklenz-analytics-events';
type ClientDrawerProps = {
client: IClient | null;
@@ -20,6 +22,7 @@ const ClientDrawer = ({ client, drawerClosed }: ClientDrawerProps) => {
const { isClientDrawerOpen } = useAppSelector(state => state.clientReducer);
const dispatch = useAppDispatch();
const [form] = Form.useForm();
+ const { trackMixpanelEvent } = useMixpanelTracking();
useEffect(() => {
if (client?.name) {
@@ -32,6 +35,7 @@ const ClientDrawer = ({ client, drawerClosed }: ClientDrawerProps) => {
if (client && client.id) {
await dispatch(updateClient({ id: client.id, body: { name: values.name } }));
} else {
+ trackMixpanelEvent(evt_settings_clients_create);
await dispatch(createClient({ name: values.name }));
}
dispatch(toggleClientDrawer());
diff --git a/worklenz-frontend/src/pages/settings/clients/clients-settings.tsx b/worklenz-frontend/src/pages/settings/clients/clients-settings.tsx
index 161a3df70..7923c69d8 100644
--- a/worklenz-frontend/src/pages/settings/clients/clients-settings.tsx
+++ b/worklenz-frontend/src/pages/settings/clients/clients-settings.tsx
@@ -31,11 +31,14 @@ import { DEFAULT_PAGE_SIZE } from '@/shared/constants';
import ClientDrawer from './client-drawer';
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import logger from '@/utils/errorLogger';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_settings_clients_visit } from '@/shared/worklenz-analytics-events';
const ClientsSettings: React.FC = () => {
const { t } = useTranslation('settings/clients');
const { clients } = useAppSelector(state => state.clientReducer);
const dispatch = useAppDispatch();
+ const { trackMixpanelEvent } = useMixpanelTracking();
useDocumentTitle('Manage Clients');
@@ -62,6 +65,10 @@ const ClientsSettings: React.FC = () => {
};
}, [pagination, searchQuery, dispatch]);
+ useEffect(() => {
+ trackMixpanelEvent(evt_settings_clients_visit);
+ }, [trackMixpanelEvent]);
+
useEffect(() => {
getClients();
}, [searchQuery]);
diff --git a/worklenz-frontend/src/pages/settings/job-titles/job-titles-drawer.tsx b/worklenz-frontend/src/pages/settings/job-titles/job-titles-drawer.tsx
index 8f3b5f410..c58f69399 100644
--- a/worklenz-frontend/src/pages/settings/job-titles/job-titles-drawer.tsx
+++ b/worklenz-frontend/src/pages/settings/job-titles/job-titles-drawer.tsx
@@ -2,6 +2,8 @@ import { Button, Drawer, Form, Input, message, Typography } from '@/shared/antd-
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.service';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_settings_job_titles_create } from '@/shared/worklenz-analytics-events';
type JobTitleDrawerProps = {
drawerOpen: boolean;
@@ -16,6 +18,7 @@ const JobTitleDrawer = ({
}: JobTitleDrawerProps) => {
const { t } = useTranslation('settings/job-titles');
const [form] = Form.useForm();
+ const { trackMixpanelEvent } = useMixpanelTracking();
useEffect(() => {
if (jobTitleId) {
@@ -46,6 +49,7 @@ const JobTitleDrawer = ({
drawerClosed();
}
} else {
+ trackMixpanelEvent(evt_settings_job_titles_create);
const response = await jobTitlesApiService.createJobTitle({ name: values.name });
if (response.done) {
drawerClosed();
diff --git a/worklenz-frontend/src/pages/settings/job-titles/job-titles-settings.tsx b/worklenz-frontend/src/pages/settings/job-titles/job-titles-settings.tsx
index 896e05bfd..2de0c8086 100644
--- a/worklenz-frontend/src/pages/settings/job-titles/job-titles-settings.tsx
+++ b/worklenz-frontend/src/pages/settings/job-titles/job-titles-settings.tsx
@@ -27,6 +27,8 @@ import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import JobTitleDrawer from './job-titles-drawer';
import logger from '@/utils/errorLogger';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_settings_job_titles_visit } from '@/shared/worklenz-analytics-events';
interface PaginationType {
current: number;
@@ -41,6 +43,7 @@ interface PaginationType {
const JobTitlesSettings = () => {
const { t } = useTranslation('settings/job-titles');
const dispatch = useAppDispatch();
+ const { trackMixpanelEvent } = useMixpanelTracking();
useDocumentTitle('Manage Job Titles');
const [selectedJobId, setSelectedJobId] = useState(null);
@@ -73,6 +76,10 @@ const JobTitlesSettings = () => {
};
}, [pagination.current, pagination.pageSize, pagination.field, pagination.order, searchQuery]);
+ useEffect(() => {
+ trackMixpanelEvent(evt_settings_job_titles_visit);
+ }, [trackMixpanelEvent]);
+
useEffect(() => {
getJobTitles();
}, [getJobTitles]);
diff --git a/worklenz-frontend/src/pages/settings/labels/LabelsSettings.tsx b/worklenz-frontend/src/pages/settings/labels/LabelsSettings.tsx
index f35113c22..659a7d63e 100644
--- a/worklenz-frontend/src/pages/settings/labels/LabelsSettings.tsx
+++ b/worklenz-frontend/src/pages/settings/labels/LabelsSettings.tsx
@@ -23,9 +23,12 @@ import CustomColorLabel from '@components/task-list-common/labelsSelector/custom
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import logger from '@/utils/errorLogger';
import LabelsDrawer from './labels-drawer';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_settings_labels_visit } from '@/shared/worklenz-analytics-events';
const LabelsSettings = () => {
const { t } = useTranslation('settings/labels');
+ const { trackMixpanelEvent } = useMixpanelTracking();
useDocumentTitle(t('pageTitle', 'Manage Labels'));
const [selectedLabelId, setSelectedLabelId] = useState(null);
@@ -55,6 +58,10 @@ const LabelsSettings = () => {
};
}, []);
+ useEffect(() => {
+ trackMixpanelEvent(evt_settings_labels_visit);
+ }, [trackMixpanelEvent]);
+
useEffect(() => {
getLabels();
}, [getLabels]);
diff --git a/worklenz-frontend/src/pages/settings/notifications/notifications-settings.tsx b/worklenz-frontend/src/pages/settings/notifications/notifications-settings.tsx
index e1575562d..dc7cd3174 100644
--- a/worklenz-frontend/src/pages/settings/notifications/notifications-settings.tsx
+++ b/worklenz-frontend/src/pages/settings/notifications/notifications-settings.tsx
@@ -6,11 +6,14 @@ import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import { INotificationSettings } from '@/types/settings/notifications.types';
import { profileSettingsApiService } from '@/api/settings/profile/profile-settings.api.service';
import logger from '@/utils/errorLogger';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_settings_notifications_visit } from '@/shared/worklenz-analytics-events';
const NotificationsSettings = () => {
const { t } = useTranslation('settings/notifications');
const [form] = Form.useForm();
const themeMode = useAppSelector(state => state.themeReducer.mode);
+ const { trackMixpanelEvent } = useMixpanelTracking();
const [notificationsSettings, setNotificationsSettings] = useState({});
const [isLoading, setIsLoading] = useState(false);
@@ -69,8 +72,9 @@ const NotificationsSettings = () => {
};
useEffect(() => {
+ trackMixpanelEvent(evt_settings_notifications_visit);
fetchNotificationsSettings();
- }, []);
+ }, [trackMixpanelEvent]);
return (
diff --git a/worklenz-frontend/src/pages/settings/task-templates/task-templates-settings.tsx b/worklenz-frontend/src/pages/settings/task-templates/task-templates-settings.tsx
index 5bf3f18e1..8f59feb66 100644
--- a/worklenz-frontend/src/pages/settings/task-templates/task-templates-settings.tsx
+++ b/worklenz-frontend/src/pages/settings/task-templates/task-templates-settings.tsx
@@ -11,6 +11,8 @@ import { ITaskTemplatesGetResponse } from '@/types/settings/task-templates.types
import logger from '@/utils/errorLogger';
import { taskTemplatesApiService } from '@/api/task-templates/task-templates.api.service';
import { calculateTimeGap } from '@/utils/calculate-time-gap';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_settings_task_templates_visit } from '@/shared/worklenz-analytics-events';
const TaskTemplatesSettings = () => {
const { t } = useTranslation('settings/task-templates');
@@ -20,6 +22,7 @@ const TaskTemplatesSettings = () => {
const [isLoading, setIsLoading] = useState(false);
const [templateId, setTemplateId] = useState(null);
const [showDrawer, setShowDrawer] = useState(false);
+ const { trackMixpanelEvent } = useMixpanelTracking();
useDocumentTitle('Task Templates');
const fetchTaskTemplates = async () => {
@@ -35,8 +38,9 @@ const TaskTemplatesSettings = () => {
};
useEffect(() => {
+ trackMixpanelEvent(evt_settings_task_templates_visit);
fetchTaskTemplates();
- }, []);
+ }, [trackMixpanelEvent]);
const handleDeleteTemplate = async (id: string) => {
try {
diff --git a/worklenz-frontend/src/pages/settings/teams/teams-settings.tsx b/worklenz-frontend/src/pages/settings/teams/teams-settings.tsx
index e6932d09f..7a7523847 100644
--- a/worklenz-frontend/src/pages/settings/teams/teams-settings.tsx
+++ b/worklenz-frontend/src/pages/settings/teams/teams-settings.tsx
@@ -11,9 +11,12 @@ import { fetchTeams } from '@features/teams/teamSlice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import { ITeamGetResponse } from '@/types/teams/team.type';
+import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
+import { evt_settings_teams_visit } from '@/shared/worklenz-analytics-events';
const TeamsSettings = () => {
const { t } = useTranslation('settings/teams');
+ const { trackMixpanelEvent } = useMixpanelTracking();
useDocumentTitle(t('title'));
const [selectedTeam, setSelectedTeam] = useState(null);
@@ -22,8 +25,9 @@ const TeamsSettings = () => {
const dispatch = useAppDispatch();
useEffect(() => {
+ trackMixpanelEvent(evt_settings_teams_visit);
dispatch(fetchTeams());
- }, [dispatch]);
+ }, [trackMixpanelEvent, dispatch]);
const columns: TableProps['columns'] = [
{
From fd0d27b7c5e3bf74f0fa0cc7a14b307a26d0355b Mon Sep 17 00:00:00 2001
From: Chamika J <75464293+chamikaJ@users.noreply.github.com>
Date: Tue, 12 Aug 2025 12:38:57 +0530
Subject: [PATCH 6/7] feat(analytics): enhance Mixpanel tracking with user
identity on initialization
- Added functionality to set Mixpanel identity for authenticated users on page load/reload.
- Captured user details such as ID, name, email, and avatar for improved tracking accuracy.
- Enhanced logging for successful identity setting during Mixpanel initialization.
---
.../src/hooks/useMixpanelTracking.tsx | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/worklenz-frontend/src/hooks/useMixpanelTracking.tsx b/worklenz-frontend/src/hooks/useMixpanelTracking.tsx
index 408cb705a..0ef85e009 100644
--- a/worklenz-frontend/src/hooks/useMixpanelTracking.tsx
+++ b/worklenz-frontend/src/hooks/useMixpanelTracking.tsx
@@ -22,13 +22,26 @@ export const useMixpanelTracking = () => {
try {
initMixpanel(token);
logger.info('Mixpanel initialized successfully for production');
+
+ // Set identity if user is already authenticated on page load/reload
+ const currentUser = auth.getCurrentSession();
+ if (currentUser?.id) {
+ mixpanel.identify(currentUser.id);
+ mixpanel.people.set({
+ $user_id: currentUser.id,
+ $name: currentUser.name,
+ $email: currentUser.email,
+ $avatar: currentUser.avatar_url,
+ });
+ logger.debug('Mixpanel identity set on initialization', currentUser.id);
+ }
} catch (error) {
logger.error('Failed to initialize Mixpanel:', error);
}
} else {
logger.info('Mixpanel not initialized - not in production environment or missing token');
}
- }, [token, isProductionEnvironment]);
+ }, [token, isProductionEnvironment, auth]);
const setIdentity = useCallback((user: any) => {
if (!isProductionEnvironment) {
From 38e7d9273a87a8bd194c0013ac77abd983e0376a Mon Sep 17 00:00:00 2001
From: Chamika J <75464293+chamikaJ@users.noreply.github.com>
Date: Thu, 14 Aug 2025 08:47:00 +0530
Subject: [PATCH 7/7] refactor(tawk): remove Tawk.to integration from frontend
and backend
- Deleted Tawk.to integration files from both the frontend and backend to streamline the codebase.
- This change eliminates unused dependencies and simplifies the project structure.
---
worklenz-backend/src/views/_tawk-to.pug | 11 ----
worklenz-frontend/src/components/TawkTo.tsx | 50 -------------------
.../src/pages/home/home-page.tsx | 1 +
.../src/pages/projects/project-list.tsx | 7 +++
4 files changed, 8 insertions(+), 61 deletions(-)
delete mode 100644 worklenz-backend/src/views/_tawk-to.pug
delete mode 100644 worklenz-frontend/src/components/TawkTo.tsx
diff --git a/worklenz-backend/src/views/_tawk-to.pug b/worklenz-backend/src/views/_tawk-to.pug
deleted file mode 100644
index 4790d6521..000000000
--- a/worklenz-backend/src/views/_tawk-to.pug
+++ /dev/null
@@ -1,11 +0,0 @@
-if !isInternalServer()
- script(type='text/javascript').
- var Tawk_API=Tawk_API||{}, Tawk_LoadStart=new Date();
- (function(){
- var s1=document.createElement("script"),s0=document.getElementsByTagName("script")[0];
- s1.async=true;
- s1.src='https://embed.tawk.to/666fc2b29a809f19fb3e837a/1i0i912sj';
- s1.charset='UTF-8';
- s1.setAttribute('crossorigin','*');
- s0.parentNode.insertBefore(s1,s0);
- })();
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/TawkTo.tsx b/worklenz-frontend/src/components/TawkTo.tsx
deleted file mode 100644
index c447a0500..000000000
--- a/worklenz-frontend/src/components/TawkTo.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { useEffect } from 'react';
-
-// Add TypeScript declarations for Tawk_API
-declare global {
- interface Window {
- Tawk_API?: any;
- Tawk_LoadStart?: Date;
- }
-}
-
-interface TawkToProps {
- propertyId: string;
- widgetId: string;
-}
-
-const TawkTo: React.FC = ({ propertyId, widgetId }) => {
- useEffect(() => {
- // Initialize tawk.to chat
- const s1 = document.createElement('script');
- s1.async = true;
- s1.src = `https://embed.tawk.to/${propertyId}/${widgetId}`;
- s1.setAttribute('crossorigin', '*');
-
- const s0 = document.getElementsByTagName('script')[0];
- s0.parentNode?.insertBefore(s1, s0);
-
- return () => {
- // Clean up when the component unmounts
- // Remove the script tag
- const tawkScript = document.querySelector(`script[src*="tawk.to/${propertyId}"]`);
- if (tawkScript && tawkScript.parentNode) {
- tawkScript.parentNode.removeChild(tawkScript);
- }
-
- // Remove the tawk.to iframe
- const tawkIframe = document.getElementById('tawk-iframe');
- if (tawkIframe) {
- tawkIframe.remove();
- }
-
- // Reset Tawk globals
- delete window.Tawk_API;
- delete window.Tawk_LoadStart;
- };
- }, [propertyId, widgetId]);
-
- return null;
-};
-
-export default TawkTo;
diff --git a/worklenz-frontend/src/pages/home/home-page.tsx b/worklenz-frontend/src/pages/home/home-page.tsx
index 37b6b618c..c4f3047c3 100644
--- a/worklenz-frontend/src/pages/home/home-page.tsx
+++ b/worklenz-frontend/src/pages/home/home-page.tsx
@@ -129,6 +129,7 @@ const HomePage = memo(() => {
{createPortal(, document.body, 'home-task-drawer')}
{createPortal( {}} />, document.body, 'project-drawer')}
+ {createPortal(, document.body, 'survey-modal')}
);
});
diff --git a/worklenz-frontend/src/pages/projects/project-list.tsx b/worklenz-frontend/src/pages/projects/project-list.tsx
index 82c4b58e5..9a00a3f9a 100644
--- a/worklenz-frontend/src/pages/projects/project-list.tsx
+++ b/worklenz-frontend/src/pages/projects/project-list.tsx
@@ -48,6 +48,7 @@ import {
PROJECT_SORT_FIELD,
PROJECT_SORT_ORDER,
} from '@/shared/constants';
+
import { IProjectFilter } from '@/types/project/project.types';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
@@ -77,6 +78,11 @@ import {
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
import ProjectGroupList from '@/components/project-list/project-group/project-group-list';
+// Lazy load the survey modal
+const SurveyPromptModal = React.lazy(() =>
+ import('@/components/survey/SurveyPromptModal').then(m => ({ default: m.SurveyPromptModal }))
+);
+
const createFilters = (items: { id: string; name: string }[]) =>
items.map(item => ({ text: item.name, value: item.id })) as ColumnFilterItem[];
@@ -893,6 +899,7 @@ const ProjectList: React.FC = () => {
{createPortal(, document.body, 'project-drawer')}
+ {createPortal(, document.body, 'project-survey-modal')}
);
};