Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const RecruiterPaymentPaddleContextProvider = ({

const { paddle, openCheckout } = usePaddlePayment({
successCallback: () => {
router.push(
router.replace(
Comment thread
capJavert marked this conversation as resolved.
`${webappUrl}recruiter/${router.query.opportunityId}/matches`,
);
},
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/features/opportunity/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ export const OPPORTUNITY_FRAGMENT = gql`
feedbackQuestions {
...OpportunityFeedbackQuestionFragment
}
subscriptionStatus
flags {
batchSize
plan
}
}
${ORGANIZATION_SHORT_FRAGMENT}
Expand Down
2 changes: 0 additions & 2 deletions packages/shared/src/features/opportunity/mockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { Opportunity } from './types';
import type { OpportunityPreviewContextType } from './context/OpportunityPreviewContext';
import { SeniorityLevel } from './protobuf/opportunity';
import { SourceMemberRole, SourceType } from '../../graphql/sources';
import { SubscriptionStatus } from '../../lib/plus';

export const mockOpportunity: Opportunity = {
id: '89f3daff-d6bb-4652-8f9c-b9f7254c9af1',
Expand Down Expand Up @@ -64,7 +63,6 @@ export const mockOpportunity: Opportunity = {
{ keyword: 'JavaScript' },
{ keyword: 'Tailwind CSS' },
],
subscriptionStatus: SubscriptionStatus.None,
};

export const mockAnonymousUserTableData: OpportunityPreviewContextType = {
Expand Down
9 changes: 4 additions & 5 deletions packages/shared/src/features/opportunity/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type { LocationType } from './protobuf/util';
import type { Connection } from '../../graphql/common';
import type { Squad } from '../../graphql/sources';
import type { TopReader } from '../../components/badges/TopReaderBadge';
import type { SubscriptionStatus } from '../../lib/plus';

export enum OpportunityMatchStatus {
Pending = 'pending',
Expand Down Expand Up @@ -87,9 +86,10 @@ export type OpportunityScreeningAnswer = {
answer: string;
};

type OpportunityFlagsPublic = {
batchSize?: number;
};
type OpportunityFlagsPublic = Partial<{
batchSize: number;
plan: string;
}>;

export type Opportunity = {
id: string;
Expand All @@ -107,7 +107,6 @@ export type Opportunity = {
keywords?: Keyword[];
questions?: OpportunityScreeningQuestion[];
feedbackQuestions?: OpportunityFeedbackQuestion[];
subscriptionStatus: SubscriptionStatus;
flags?: OpportunityFlagsPublic;
};

Expand Down
8 changes: 3 additions & 5 deletions packages/shared/src/lib/schema/opportunity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,6 @@ export const opportunityEditInfoSchema = z.object({
),
),
}),
organization: z
.object({
name: z.string().nonempty().max(60),
})
.nullish(),
});

export const createOpportunityEditContentSchema = ({
Expand Down Expand Up @@ -111,6 +106,9 @@ export const opportunityEditQuestionsSchema = z.object({

export const opportunityEditStep1Schema = opportunityEditInfoSchema.extend({
content: opportunityEditContentSchema.shape.content,
organization: z.object({
name: z.string().nonempty(),
}),
});

export const opportunityEditStep2Schema = opportunityEditQuestionsSchema.extend(
Expand Down
50 changes: 47 additions & 3 deletions packages/webapp/pages/recruiter/[opportunityId]/matches/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactElement } from 'react';
import React from 'react';
import { useRouter } from 'next/router';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ConnectHeader } from '@dailydotdev/shared/src/components/recruiter/ConnectHeader';
import { ConnectProgress } from '@dailydotdev/shared/src/components/recruiter/ConnectProgress';
import { Loader } from '@dailydotdev/shared/src/components/Loader';
Expand All @@ -25,13 +25,53 @@ import {
import { GenericLoaderSpinner } from '@dailydotdev/shared/src/components/utilities/loaders';
import { IconSize } from '@dailydotdev/shared/src/components/Icon';

import { opportunityByIdOptions } from '@dailydotdev/shared/src/features/opportunity/queries';
import { oneMinute } from '@dailydotdev/shared/src/lib/dateFormat';
import { transactionRefetchIntervalMs } from '@dailydotdev/shared/src/graphql/njord';
import { OpportunityState } from '@dailydotdev/shared/src/features/opportunity/protobuf/opportunity';
import { getLayout } from '../../../../components/layouts/RecruiterSelfServeLayout';

function RecruiterMatchesPage(): ReactElement {
const router = useRouter();
const { opportunityId } = router.query;
const queryClient = useQueryClient();

const { data: opportunity } = useQuery({
...opportunityByIdOptions({
id: opportunityId as string,
}),
refetchInterval: (query) => {
const retries = Math.max(
query.state.dataUpdateCount,
query.state.fetchFailureCount,
);

// transactions are mostly processed withing few seconds
// so for now we stop retrying after 1 minute
const maxRetries = (oneMinute * 1000) / transactionRefetchIntervalMs;

if (retries > maxRetries) {
return false;
}

const queryError = query.state.error;

// in case of query error keep refetching until maxRetries is reached
if (queryError) {
return transactionRefetchIntervalMs;
}

const isReadyForMatches =
query.state.data?.state !== OpportunityState.DRAFT;

if (isReadyForMatches) {
return false;
}

return transactionRefetchIntervalMs;
},
});

const { allMatches, isLoading, data } = useOpportunityMatches({
opportunityId: opportunityId as string,
status: 'candidate_accepted',
Expand Down Expand Up @@ -95,6 +135,8 @@ function RecruiterMatchesPage(): ReactElement {
);
}

const isReadyForMatches = opportunity?.state !== OpportunityState.DRAFT;

return (
<OpportunityProvider opportunityId={opportunityId as string}>
<div className="flex flex-1 flex-col">
Expand Down Expand Up @@ -142,8 +184,10 @@ function RecruiterMatchesPage(): ReactElement {
color={TypographyColor.Tertiary}
center
>
We’re already talking to the right developers for you — all
opt-in, all high-intent.
{isReadyForMatches &&
"We're already talking to the right developers for you — all opt-in, all high-intent."}
{!isReadyForMatches &&
'We are gonna start reaching to developers soon, we are still processing your data and payment...'}
</Typography>
</div>
</>
Expand Down
17 changes: 17 additions & 0 deletions packages/webapp/pages/recruiter/[opportunityId]/payment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext';
import { recruiterPricesQueryOptions } from '@dailydotdev/shared/src/features/opportunity/graphql';
import { useQuery } from '@tanstack/react-query';
import { Loader } from '@dailydotdev/shared/src/components/Loader';
import { webappUrl } from '@dailydotdev/shared/src/lib/constants';
import { useToastNotification } from '@dailydotdev/shared/src/hooks';

const RecruiterPaymentPage = (): ReactElement => {
const router = useRouter();
const checkoutRef = useRef<HTMLDivElement>(null);
const { openCheckout, selectedProduct } = useRecruiterPaymentContext();
const { opportunity } = useOpportunityPreviewContext();
const { displayToast } = useToastNotification();

useEffect(() => {
if (!opportunity) {
Expand All @@ -47,6 +50,20 @@ const RecruiterPaymentPage = (): ReactElement => {
});
}, [selectedProduct, openCheckout, opportunity]);

useEffect(() => {
if (!opportunity) {
return;
}

if (!opportunity.organization) {
router.replace(`${webappUrl}recruiter/${opportunity.id}/prepare`);

displayToast(
'Organization info missing, please enter it to proceed with payment.',
);
}
}, [displayToast, opportunity, router]);

const handleBack = () => {
router.back();
};
Expand Down
39 changes: 37 additions & 2 deletions packages/webapp/pages/recruiter/[opportunityId]/prepare.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,54 @@ import {
} from '@dailydotdev/shared/src/components/opportunity/OpportunityEditContext';
import { useRouter } from 'next/router';
import { webappUrl } from '@dailydotdev/shared/src/lib/constants';
import { opportunityEditStep1Schema } from '@dailydotdev/shared/src/lib/schema/opportunity';
import { usePrompt } from '@dailydotdev/shared/src/hooks/usePrompt';
import { labels } from '@dailydotdev/shared/src/lib/labels';
import { getLayout } from '../../../components/layouts/RecruiterSelfServeLayout';
import JobPage from '../../jobs/[id]';

function PreparePage(): ReactElement {
const router = useRouter();
const { opportunityId } = useOpportunityEditContext();
const { opportunityId, onValidateOpportunity } = useOpportunityEditContext();
const { showPrompt } = usePrompt();

return (
<div className="flex flex-1 flex-col">
<RecruiterHeader
headerButton={{
text: 'Outreach settings',
onClick: () => {
onClick: async () => {
const result = onValidateOpportunity({
schema: opportunityEditStep1Schema,
});

if (result.error) {
await showPrompt({
title: labels.opportunity.requiredMissingNotice.title,
description: (
<div className="flex flex-col gap-4">
<span>
{labels.opportunity.requiredMissingNotice.description}
</span>
<ul className="text-text-tertiary">
{result.error.issues.map((issue) => {
const path = issue.path.join('.');

return <li key={path}>• {path}</li>;
})}
</ul>
</div>
),
okButton: {
className: '!w-full',
title: labels.opportunity.requiredMissingNotice.okButton,
},
cancelButton: null,
});

return;
}

router.push(`${webappUrl}recruiter/${opportunityId}/plans`);
},
}}
Expand Down