From f0a9a84800cd2f6fbc33d2026e6f3b3ea3eaf004 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 7 Nov 2025 15:36:39 +0200 Subject: [PATCH 1/3] feat: start on job opportunity modal --- .../src/components/modals/BootPopups.tsx | 27 ++++++ .../components/modals/JobOpportunityModal.tsx | 95 +++++++++++++++++++ .../shared/src/components/modals/common.tsx | 8 ++ .../src/components/modals/common/types.ts | 1 + packages/shared/src/graphql/alerts.ts | 5 + 5 files changed, 136 insertions(+) create mode 100644 packages/shared/src/components/modals/JobOpportunityModal.tsx diff --git a/packages/shared/src/components/modals/BootPopups.tsx b/packages/shared/src/components/modals/BootPopups.tsx index 081827bc2b3..10e2f510e34 100644 --- a/packages/shared/src/components/modals/BootPopups.tsx +++ b/packages/shared/src/components/modals/BootPopups.tsx @@ -310,6 +310,33 @@ export const BootPopups = (): ReactElement => { }); }, [alerts.showTopReader, logEvent, updateAlerts, updateLastBootPopup]); + /** + * Job opportunity modal + */ + useEffect(() => { + if (!alerts?.opportunityId || alerts?.flags?.hasSeenOpportunity) { + return; + } + + console.log('should add it'); + + addBootPopup({ + type: LazyModal.JobOpportunity, + props: { + opportunityId: alerts.opportunityId, + onAfterClose: () => { + updateAlerts({ flags: { hasSeenOpportunity: true } }); + updateLastBootPopup(); + }, + }, + }); + }, [ + alerts.opportunityId, + alerts?.flags?.hasSeenOpportunity, + updateAlerts, + updateLastBootPopup, + ]); + /** * Actual rendering of the boot popup that's first in line */ diff --git a/packages/shared/src/components/modals/JobOpportunityModal.tsx b/packages/shared/src/components/modals/JobOpportunityModal.tsx new file mode 100644 index 00000000000..01234d3c1d0 --- /dev/null +++ b/packages/shared/src/components/modals/JobOpportunityModal.tsx @@ -0,0 +1,95 @@ +import type { MouseEvent, ReactElement } from 'react'; +import React from 'react'; +import type { ModalProps } from './common/Modal'; +import { Modal } from './common/Modal'; +import { Button, ButtonVariant } from '../buttons/Button'; +import { ModalClose } from './common/ModalClose'; +import { Image } from '../image/Image'; +import { opportunityLiveIllustration } from '../../lib/image'; +import Link from '../utilities/Link'; +import { opportunityUrl } from '../../lib/constants'; + +export interface JobOpportunityModalProps extends ModalProps { + opportunityId: string; +} + +export const JobOpportunityModal = ({ + opportunityId, + onRequestClose, + ...modalProps +}: JobOpportunityModalProps): ReactElement => { + const onShowNow = (event: MouseEvent) => { + onRequestClose(event); + }; + + const onMaybeLater = (event: MouseEvent) => { + onRequestClose(event); + }; + + return ( + + + + +
+
+ Job opportunity +
+ +
+

+ + YOU GOT + +
+ + A NEW + +
+ + JOB OFFER + +

+

+ We think this role deserves your attention, but your decision is + private. If it's a fit, we'll handle it quietly. If not, it's gone + for good. +

+
+ +
+ + + + +
+
+
+
+ ); +}; + +export default JobOpportunityModal; diff --git a/packages/shared/src/components/modals/common.tsx b/packages/shared/src/components/modals/common.tsx index c2b703fd41d..edac1f153a9 100644 --- a/packages/shared/src/components/modals/common.tsx +++ b/packages/shared/src/components/modals/common.tsx @@ -334,6 +334,13 @@ const OpportunityEditModal = dynamic(() => ).then((mod) => mod.OpportunityEditModal), ); +const JobOpportunityModal = dynamic( + () => + import( + /* webpackChunkName: "jobOpportunityModal" */ './JobOpportunityModal' + ), +); + export const modals = { [LazyModal.SquadMember]: SquadMemberModal, [LazyModal.UpvotedPopup]: UpvotedPopupModal, @@ -389,6 +396,7 @@ export const modals = { [LazyModal.ActionSuccess]: ActionSuccessModal, [LazyModal.SquadNotificationSettings]: SquadNotificationSettingsModal, [LazyModal.OpportunityEdit]: OpportunityEditModal, + [LazyModal.JobOpportunity]: JobOpportunityModal, }; type GetComponentProps = T extends diff --git a/packages/shared/src/components/modals/common/types.ts b/packages/shared/src/components/modals/common/types.ts index cd88777dc6e..6cc4f20770d 100644 --- a/packages/shared/src/components/modals/common/types.ts +++ b/packages/shared/src/components/modals/common/types.ts @@ -79,6 +79,7 @@ export enum LazyModal { ActionSuccess = 'actionSuccess', SquadNotificationSettings = 'squadNotificationSettings', OpportunityEdit = 'opportunityEdit', + JobOpportunity = 'jobOpportunity', } export type ModalTabItem = { diff --git a/packages/shared/src/graphql/alerts.ts b/packages/shared/src/graphql/alerts.ts index 626a2c8b71e..adb9272f54d 100644 --- a/packages/shared/src/graphql/alerts.ts +++ b/packages/shared/src/graphql/alerts.ts @@ -1,6 +1,10 @@ import { gql } from 'graphql-request'; import type { Opportunity } from '../features/opportunity/types'; +export type AlertsFlags = { + hasSeenOpportunity?: boolean; +}; + export type Alerts = { filter?: boolean; rankLastSeen?: Date; @@ -20,6 +24,7 @@ export type Alerts = { showTopReader?: boolean; briefBannerLastSeen?: Date; opportunityId?: Opportunity['id'] | null; + flags?: AlertsFlags; }; export type AlertsUpdate = Omit; From 2bc85b6339b04b8c6a6f9d10748513b082c83fb3 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Mon, 10 Nov 2025 14:59:40 +0200 Subject: [PATCH 2/3] fix: new job opp popup --- .../src/components/modals/BootPopups.tsx | 2 - .../components/modals/JobOpportunityModal.tsx | 105 ++++++++---------- .../shared/src/hooks/utils/useThemedAsset.ts | 8 ++ packages/shared/src/lib/image.ts | 8 ++ 4 files changed, 64 insertions(+), 59 deletions(-) diff --git a/packages/shared/src/components/modals/BootPopups.tsx b/packages/shared/src/components/modals/BootPopups.tsx index 10e2f510e34..03d78ab8f18 100644 --- a/packages/shared/src/components/modals/BootPopups.tsx +++ b/packages/shared/src/components/modals/BootPopups.tsx @@ -318,8 +318,6 @@ export const BootPopups = (): ReactElement => { return; } - console.log('should add it'); - addBootPopup({ type: LazyModal.JobOpportunity, props: { diff --git a/packages/shared/src/components/modals/JobOpportunityModal.tsx b/packages/shared/src/components/modals/JobOpportunityModal.tsx index 01234d3c1d0..2f4249709e5 100644 --- a/packages/shared/src/components/modals/JobOpportunityModal.tsx +++ b/packages/shared/src/components/modals/JobOpportunityModal.tsx @@ -3,11 +3,13 @@ import React from 'react'; import type { ModalProps } from './common/Modal'; import { Modal } from './common/Modal'; import { Button, ButtonVariant } from '../buttons/Button'; -import { ModalClose } from './common/ModalClose'; import { Image } from '../image/Image'; -import { opportunityLiveIllustration } from '../../lib/image'; import Link from '../utilities/Link'; import { opportunityUrl } from '../../lib/constants'; +import { useViewSize, ViewSize } from '../../hooks'; +import { useThemedAsset } from '../../hooks/utils'; +import { Typography, TypographyType } from '../typography/Typography'; +import { MoveToIcon } from '../icons'; export interface JobOpportunityModalProps extends ModalProps { opportunityId: string; @@ -18,6 +20,8 @@ export const JobOpportunityModal = ({ onRequestClose, ...modalProps }: JobOpportunityModalProps): ReactElement => { + const isMobile = useViewSize(ViewSize.MobileL); + const { jobOfferDesktop, jobOfferMobile } = useThemedAsset(); const onShowNow = (event: MouseEvent) => { onRequestClose(event); }; @@ -27,68 +31,55 @@ export const JobOpportunityModal = ({ }; return ( - - - - -
-
- Job opportunity -
- -
-

- - YOU GOT - -
- - A NEW - -
- - JOB OFFER - -

-

+ <> +

+ test +
+ + +
+
+ Job offer for you +
+ We think this role deserves your attention, but your decision is - private. If it's a fit, we'll handle it quietly. If not, it's gone + private. If it’s a fit, we’ll handle it quietly. If not, it’s gone for good. -

-
- -
- + +
+ + + - - +
-
- - + + + ); }; diff --git a/packages/shared/src/hooks/utils/useThemedAsset.ts b/packages/shared/src/hooks/utils/useThemedAsset.ts index df13200cf29..86e8b6a9754 100644 --- a/packages/shared/src/hooks/utils/useThemedAsset.ts +++ b/packages/shared/src/hooks/utils/useThemedAsset.ts @@ -18,6 +18,10 @@ import { boostNewPostBannerLight, jobsWelcomeDarkMode, jobsWelcomeLightMode, + jobOfferLightDesktop, + jobOfferDarkDesktop, + jobOfferDarkMobile, + jobOfferLightMobile, } from '../../lib/image'; interface UseAsset { @@ -30,6 +34,8 @@ interface UseAsset { slackIntegrationHeader: string; gardrError: string; jobsWelcome: string; + jobOfferDesktop: string; + jobOfferMobile: string; } export const useIsLightTheme = (): boolean => { @@ -70,5 +76,7 @@ export const useThemedAsset = (): UseAsset => { ? cloudinaryGenericErrorLight : cloudinaryGenericErrorDark, jobsWelcome: isLight ? jobsWelcomeLightMode : jobsWelcomeDarkMode, + jobOfferDesktop: isLight ? jobOfferLightDesktop : jobOfferDarkDesktop, + jobOfferMobile: isLight ? jobOfferLightMobile : jobOfferDarkMobile, }; }; diff --git a/packages/shared/src/lib/image.ts b/packages/shared/src/lib/image.ts index bd55e4e50d4..dc92c890ec7 100644 --- a/packages/shared/src/lib/image.ts +++ b/packages/shared/src/lib/image.ts @@ -375,3 +375,11 @@ export const recruiterSpamCampaignSEO = 'https://media.daily.dev/image/upload/s--PBa-49xs--/f_auto/v1759245831/public/Recruiter%20-%206'; export const adFaviconPlaceholder = 'https://media.daily.dev/image/upload/s--SOLIE7Bc--/f_auto/v1761801782/webapp/daily.dev_-_Boost_Icon'; +export const jobOfferDarkDesktop = + 'https://media.daily.dev/image/upload/s--NfynfZap--/f_auto/v1762762600/public/new-offer-dark-web'; +export const jobOfferLightDesktop = + 'https://media.daily.dev/image/upload/s--NfynfZap--/f_auto/v1762762601/public/new-offer-light-web'; +export const jobOfferDarkMobile = + 'https://media.daily.dev/image/upload/s--Fucq4fUY--/f_auto/v1762762600/public/new-offer-dark-mobile'; +export const jobOfferLightMobile = + 'https://media.daily.dev/image/upload/s--kgtQcvdc--/f_auto/v1762762601/public/new-offer-light-mobile'; From efb3490e6ce3f1cd6124d97313209d10608d96d2 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Mon, 10 Nov 2025 16:59:54 +0200 Subject: [PATCH 3/3] fix: tracking --- .../components/modals/JobOpportunityModal.tsx | 40 +++++++++++++++---- .../components/JobOpportunityButton.tsx | 4 +- packages/shared/src/lib/log.ts | 2 + 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/shared/src/components/modals/JobOpportunityModal.tsx b/packages/shared/src/components/modals/JobOpportunityModal.tsx index 2f4249709e5..db6bf231a0d 100644 --- a/packages/shared/src/components/modals/JobOpportunityModal.tsx +++ b/packages/shared/src/components/modals/JobOpportunityModal.tsx @@ -1,5 +1,5 @@ import type { MouseEvent, ReactElement } from 'react'; -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import type { ModalProps } from './common/Modal'; import { Modal } from './common/Modal'; import { Button, ButtonVariant } from '../buttons/Button'; @@ -10,6 +10,8 @@ import { useViewSize, ViewSize } from '../../hooks'; import { useThemedAsset } from '../../hooks/utils'; import { Typography, TypographyType } from '../typography/Typography'; import { MoveToIcon } from '../icons'; +import { useLogContext } from '../../contexts/LogContext'; +import { LogEvent, TargetId } from '../../lib/log'; export interface JobOpportunityModalProps extends ModalProps { opportunityId: string; @@ -22,14 +24,37 @@ export const JobOpportunityModal = ({ }: JobOpportunityModalProps): ReactElement => { const isMobile = useViewSize(ViewSize.MobileL); const { jobOfferDesktop, jobOfferMobile } = useThemedAsset(); - const onShowNow = (event: MouseEvent) => { - onRequestClose(event); - }; + const { logEvent } = useLogContext(); + const logRef = useRef(); + const hasLoggedRef = useRef(false); + logRef.current = logEvent; + + const logExtraPayload = JSON.stringify({ + count: 1, // always 1 for now + }); - const onMaybeLater = (event: MouseEvent) => { + const onClick = (event: MouseEvent) => { + logRef.current({ + event_name: LogEvent.ClickOpportunityNudge, + target_id: TargetId.Fullscreen, + extra: logExtraPayload, + }); onRequestClose(event); }; + useEffect(() => { + if (hasLoggedRef.current) { + return; + } + + logRef.current({ + event_name: LogEvent.ImpressionOpportunityNudge, + target_id: TargetId.Fullscreen, + extra: logExtraPayload, + }); + hasLoggedRef.current = true; + }, [logExtraPayload]); + return ( <>
@@ -42,6 +67,7 @@ export const JobOpportunityModal = ({ onRequestClose={onRequestClose} className="!border-transparent !bg-transparent !shadow-none" overlayClassName="!bg-transparent" + isDrawerOnMobile >
@@ -63,7 +89,7 @@ export const JobOpportunityModal = ({ tag="a" className="w-full gap-2" variant={ButtonVariant.Primary} - onClick={onShowNow} + onClick={onClick} > Show me now @@ -71,7 +97,7 @@ export const JobOpportunityModal = ({ diff --git a/packages/shared/src/features/opportunity/components/JobOpportunityButton.tsx b/packages/shared/src/features/opportunity/components/JobOpportunityButton.tsx index 5f8a96a4b7c..2c16388e6b9 100644 --- a/packages/shared/src/features/opportunity/components/JobOpportunityButton.tsx +++ b/packages/shared/src/features/opportunity/components/JobOpportunityButton.tsx @@ -11,7 +11,7 @@ import Link from '../../../components/utilities/Link'; import { useViewSize, ViewSize } from '../../../hooks'; import { useAlertsContext } from '../../../contexts/AlertContext'; import { useLogContext } from '../../../contexts/LogContext'; -import { LogEvent } from '../../../lib/log'; +import { LogEvent, TargetId } from '../../../lib/log'; import { useFeaturesReadyContext } from '../../../components/GrowthBookProvider'; import { opportunityButtonCopy } from '../../../lib/featureManagement'; @@ -40,6 +40,7 @@ export const JobOpportunityButton = ({ const handleClick = (): void => { logRef.current({ event_name: LogEvent.ClickOpportunityNudge, + target_id: TargetId.HomepageButton, extra: logExtraPayload, }); }; @@ -51,6 +52,7 @@ export const JobOpportunityButton = ({ logRef.current({ event_name: LogEvent.ImpressionOpportunityNudge, + target_id: TargetId.HomepageButton, extra: logExtraPayload, }); hasLoggedRef.current = true; diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index 1d61aee47ea..55c9d438252 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -440,6 +440,8 @@ export enum TargetId { OpportunityUnavailablePage = 'opportunity unavailable page', OpportunityWelcomePage = 'opportunity welcome page', ProfileSettingsMenu = 'profile settings menu', + HomepageButton = 'homepage button', + Fullscreen = 'fullscreen', } export enum NotificationChannel {