Skip to content

Commit 723d8a9

Browse files
authored
feat: job opportunity modal (#5054)
1 parent 6de36e5 commit 723d8a9

9 files changed

Lines changed: 172 additions & 1 deletion

File tree

packages/shared/src/components/modals/BootPopups.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,31 @@ export const BootPopups = (): ReactElement => {
310310
});
311311
}, [alerts.showTopReader, logEvent, updateAlerts, updateLastBootPopup]);
312312

313+
/**
314+
* Job opportunity modal
315+
*/
316+
useEffect(() => {
317+
if (!alerts?.opportunityId || alerts?.flags?.hasSeenOpportunity) {
318+
return;
319+
}
320+
321+
addBootPopup({
322+
type: LazyModal.JobOpportunity,
323+
props: {
324+
opportunityId: alerts.opportunityId,
325+
onAfterClose: () => {
326+
updateAlerts({ flags: { hasSeenOpportunity: true } });
327+
updateLastBootPopup();
328+
},
329+
},
330+
});
331+
}, [
332+
alerts.opportunityId,
333+
alerts?.flags?.hasSeenOpportunity,
334+
updateAlerts,
335+
updateLastBootPopup,
336+
]);
337+
313338
/**
314339
* Actual rendering of the boot popup that's first in line
315340
*/
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { MouseEvent, ReactElement } from 'react';
2+
import React, { useEffect, useRef } from 'react';
3+
import type { ModalProps } from './common/Modal';
4+
import { Modal } from './common/Modal';
5+
import { Button, ButtonVariant } from '../buttons/Button';
6+
import { Image } from '../image/Image';
7+
import Link from '../utilities/Link';
8+
import { opportunityUrl } from '../../lib/constants';
9+
import { useViewSize, ViewSize } from '../../hooks';
10+
import { useThemedAsset } from '../../hooks/utils';
11+
import { Typography, TypographyType } from '../typography/Typography';
12+
import { MoveToIcon } from '../icons';
13+
import { useLogContext } from '../../contexts/LogContext';
14+
import { LogEvent, TargetId } from '../../lib/log';
15+
16+
export interface JobOpportunityModalProps extends ModalProps {
17+
opportunityId: string;
18+
}
19+
20+
export const JobOpportunityModal = ({
21+
opportunityId,
22+
onRequestClose,
23+
...modalProps
24+
}: JobOpportunityModalProps): ReactElement => {
25+
const isMobile = useViewSize(ViewSize.MobileL);
26+
const { jobOfferDesktop, jobOfferMobile } = useThemedAsset();
27+
const { logEvent } = useLogContext();
28+
const logRef = useRef<typeof logEvent>();
29+
const hasLoggedRef = useRef(false);
30+
logRef.current = logEvent;
31+
32+
const logExtraPayload = JSON.stringify({
33+
count: 1, // always 1 for now
34+
});
35+
36+
const onClick = (event: MouseEvent) => {
37+
logRef.current({
38+
event_name: LogEvent.ClickOpportunityNudge,
39+
target_id: TargetId.Fullscreen,
40+
extra: logExtraPayload,
41+
});
42+
onRequestClose(event);
43+
};
44+
45+
useEffect(() => {
46+
if (hasLoggedRef.current) {
47+
return;
48+
}
49+
50+
logRef.current({
51+
event_name: LogEvent.ImpressionOpportunityNudge,
52+
target_id: TargetId.Fullscreen,
53+
extra: logExtraPayload,
54+
});
55+
hasLoggedRef.current = true;
56+
}, [logExtraPayload]);
57+
58+
return (
59+
<>
60+
<div className="fixed z-header size-full rounded-[63.75rem] bg-background-default blur-[6.875rem]">
61+
test
62+
</div>
63+
<Modal
64+
{...modalProps}
65+
kind={Modal.Kind.FlexibleCenter}
66+
size={Modal.Size.Medium}
67+
onRequestClose={onRequestClose}
68+
className="!border-transparent !bg-transparent !shadow-none"
69+
overlayClassName="!bg-transparent"
70+
isDrawerOnMobile
71+
>
72+
<Modal.Body className="items-center overflow-hidden !p-0">
73+
<div className="flex h-full flex-col items-start justify-center gap-6 px-6 py-10 tablet:items-center tablet:px-10 tablet:py-14">
74+
<div className="relative flex items-center justify-center">
75+
<Image
76+
src={isMobile ? jobOfferMobile : jobOfferDesktop}
77+
alt="Job offer for you"
78+
className="h-72 w-auto tablet:h-64"
79+
/>
80+
</div>
81+
<Typography type={TypographyType.Title3}>
82+
We think this role deserves your attention, but your decision is
83+
private. If it’s a fit, we’ll handle it quietly. If not, it’s gone
84+
for good.
85+
</Typography>
86+
<div className="flex w-full flex-col gap-3">
87+
<Link href={`${opportunityUrl}/${opportunityId}`} passHref>
88+
<Button
89+
tag="a"
90+
className="w-full gap-2"
91+
variant={ButtonVariant.Primary}
92+
onClick={onClick}
93+
>
94+
Show me now <MoveToIcon />
95+
</Button>
96+
</Link>
97+
<Button
98+
className="w-full"
99+
variant={ButtonVariant.Float}
100+
onClick={onClick}
101+
>
102+
Maybe later
103+
</Button>
104+
</div>
105+
</div>
106+
</Modal.Body>
107+
</Modal>
108+
</>
109+
);
110+
};
111+
112+
export default JobOpportunityModal;

packages/shared/src/components/modals/common.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,13 @@ const OpportunityEditModal = dynamic(() =>
334334
).then((mod) => mod.OpportunityEditModal),
335335
);
336336

337+
const JobOpportunityModal = dynamic(
338+
() =>
339+
import(
340+
/* webpackChunkName: "jobOpportunityModal" */ './JobOpportunityModal'
341+
),
342+
);
343+
337344
export const modals = {
338345
[LazyModal.SquadMember]: SquadMemberModal,
339346
[LazyModal.UpvotedPopup]: UpvotedPopupModal,
@@ -389,6 +396,7 @@ export const modals = {
389396
[LazyModal.ActionSuccess]: ActionSuccessModal,
390397
[LazyModal.SquadNotificationSettings]: SquadNotificationSettingsModal,
391398
[LazyModal.OpportunityEdit]: OpportunityEditModal,
399+
[LazyModal.JobOpportunity]: JobOpportunityModal,
392400
};
393401

394402
type GetComponentProps<T> = T extends

packages/shared/src/components/modals/common/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export enum LazyModal {
7979
ActionSuccess = 'actionSuccess',
8080
SquadNotificationSettings = 'squadNotificationSettings',
8181
OpportunityEdit = 'opportunityEdit',
82+
JobOpportunity = 'jobOpportunity',
8283
}
8384

8485
export type ModalTabItem = {

packages/shared/src/features/opportunity/components/JobOpportunityButton.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Link from '../../../components/utilities/Link';
1111
import { useViewSize, ViewSize } from '../../../hooks';
1212
import { useAlertsContext } from '../../../contexts/AlertContext';
1313
import { useLogContext } from '../../../contexts/LogContext';
14-
import { LogEvent } from '../../../lib/log';
14+
import { LogEvent, TargetId } from '../../../lib/log';
1515
import { useFeaturesReadyContext } from '../../../components/GrowthBookProvider';
1616
import { opportunityButtonCopy } from '../../../lib/featureManagement';
1717

@@ -40,6 +40,7 @@ export const JobOpportunityButton = ({
4040
const handleClick = (): void => {
4141
logRef.current({
4242
event_name: LogEvent.ClickOpportunityNudge,
43+
target_id: TargetId.HomepageButton,
4344
extra: logExtraPayload,
4445
});
4546
};
@@ -51,6 +52,7 @@ export const JobOpportunityButton = ({
5152

5253
logRef.current({
5354
event_name: LogEvent.ImpressionOpportunityNudge,
55+
target_id: TargetId.HomepageButton,
5456
extra: logExtraPayload,
5557
});
5658
hasLoggedRef.current = true;

packages/shared/src/graphql/alerts.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { gql } from 'graphql-request';
22
import type { Opportunity } from '../features/opportunity/types';
33

4+
export type AlertsFlags = {
5+
hasSeenOpportunity?: boolean;
6+
};
7+
48
export type Alerts = {
59
filter?: boolean;
610
rankLastSeen?: Date;
@@ -20,6 +24,7 @@ export type Alerts = {
2024
showTopReader?: boolean;
2125
briefBannerLastSeen?: Date;
2226
opportunityId?: Opportunity['id'] | null;
27+
flags?: AlertsFlags;
2328
};
2429

2530
export type AlertsUpdate = Omit<Alerts, 'changelog' | 'banner'>;

packages/shared/src/hooks/utils/useThemedAsset.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import {
1818
boostNewPostBannerLight,
1919
jobsWelcomeDarkMode,
2020
jobsWelcomeLightMode,
21+
jobOfferLightDesktop,
22+
jobOfferDarkDesktop,
23+
jobOfferDarkMobile,
24+
jobOfferLightMobile,
2125
} from '../../lib/image';
2226

2327
interface UseAsset {
@@ -30,6 +34,8 @@ interface UseAsset {
3034
slackIntegrationHeader: string;
3135
gardrError: string;
3236
jobsWelcome: string;
37+
jobOfferDesktop: string;
38+
jobOfferMobile: string;
3339
}
3440

3541
export const useIsLightTheme = (): boolean => {
@@ -70,5 +76,7 @@ export const useThemedAsset = (): UseAsset => {
7076
? cloudinaryGenericErrorLight
7177
: cloudinaryGenericErrorDark,
7278
jobsWelcome: isLight ? jobsWelcomeLightMode : jobsWelcomeDarkMode,
79+
jobOfferDesktop: isLight ? jobOfferLightDesktop : jobOfferDarkDesktop,
80+
jobOfferMobile: isLight ? jobOfferLightMobile : jobOfferDarkMobile,
7381
};
7482
};

packages/shared/src/lib/image.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,11 @@ export const recruiterSpamCampaignSEO =
375375
'https://media.daily.dev/image/upload/s--PBa-49xs--/f_auto/v1759245831/public/Recruiter%20-%206';
376376
export const adFaviconPlaceholder =
377377
'https://media.daily.dev/image/upload/s--SOLIE7Bc--/f_auto/v1761801782/webapp/daily.dev_-_Boost_Icon';
378+
export const jobOfferDarkDesktop =
379+
'https://media.daily.dev/image/upload/s--NfynfZap--/f_auto/v1762762600/public/new-offer-dark-web';
380+
export const jobOfferLightDesktop =
381+
'https://media.daily.dev/image/upload/s--NfynfZap--/f_auto/v1762762601/public/new-offer-light-web';
382+
export const jobOfferDarkMobile =
383+
'https://media.daily.dev/image/upload/s--Fucq4fUY--/f_auto/v1762762600/public/new-offer-dark-mobile';
384+
export const jobOfferLightMobile =
385+
'https://media.daily.dev/image/upload/s--kgtQcvdc--/f_auto/v1762762601/public/new-offer-light-mobile';

packages/shared/src/lib/log.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,8 @@ export enum TargetId {
440440
OpportunityUnavailablePage = 'opportunity unavailable page',
441441
OpportunityWelcomePage = 'opportunity welcome page',
442442
ProfileSettingsMenu = 'profile settings menu',
443+
HomepageButton = 'homepage button',
444+
Fullscreen = 'fullscreen',
443445
}
444446

445447
export enum NotificationChannel {

0 commit comments

Comments
 (0)