diff --git a/packages/shared/src/components/modals/BootPopups.tsx b/packages/shared/src/components/modals/BootPopups.tsx index 081827bc2b3..03d78ab8f18 100644 --- a/packages/shared/src/components/modals/BootPopups.tsx +++ b/packages/shared/src/components/modals/BootPopups.tsx @@ -310,6 +310,31 @@ export const BootPopups = (): ReactElement => { }); }, [alerts.showTopReader, logEvent, updateAlerts, updateLastBootPopup]); + /** + * Job opportunity modal + */ + useEffect(() => { + if (!alerts?.opportunityId || alerts?.flags?.hasSeenOpportunity) { + return; + } + + 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..db6bf231a0d --- /dev/null +++ b/packages/shared/src/components/modals/JobOpportunityModal.tsx @@ -0,0 +1,112 @@ +import type { MouseEvent, ReactElement } 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'; +import { Image } from '../image/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'; +import { useLogContext } from '../../contexts/LogContext'; +import { LogEvent, TargetId } from '../../lib/log'; + +export interface JobOpportunityModalProps extends ModalProps { + opportunityId: string; +} + +export const JobOpportunityModal = ({ + opportunityId, + onRequestClose, + ...modalProps +}: JobOpportunityModalProps): ReactElement => { + const isMobile = useViewSize(ViewSize.MobileL); + const { jobOfferDesktop, jobOfferMobile } = useThemedAsset(); + const { logEvent } = useLogContext(); + const logRef = useRef(); + const hasLoggedRef = useRef(false); + logRef.current = logEvent; + + const logExtraPayload = JSON.stringify({ + count: 1, // always 1 for now + }); + + 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 ( + <> +
+ 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 + 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/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/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; 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'; 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 {