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
12 changes: 5 additions & 7 deletions packages/shared/src/components/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ const DevPlusIcon = dynamic(() =>
),
);

const LogoRecruiterSvg = dynamic(() =>
import(
/* webpackChunkName: "logoRecruiterSvg" */ '../svg/LogoRecruiterSvg'
).then((mod) => mod.LogoRecruiterSvg),
);

export enum LogoPosition {
Absolute = 'absolute',
Relative = 'relative',
Expand Down Expand Up @@ -152,7 +146,11 @@ export default function Logo({
fallback={LogoText}
/>
)}
{isRecruiter && !compact && <LogoRecruiterSvg />}
{isRecruiter && !compact && (
<span className="hidden rounded-6 border border-accent-cabbage-subtler bg-accent-cabbage-flat px-1.5 py-0.5 font-bold uppercase tracking-wider text-accent-cabbage-default typo-caption2 laptop:inline-block">
Recruiter
</span>
)}
</a>
</LinkWithTooltip>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import { IconSize } from '../Icon';

const LOADING_STEPS = [
'Analyzing your job description (this may take a minute)',
'Mapping skills, requirements, and intent',
'Scanning the daily.dev network...',
'Extracting skills and requirements',
'Finding matches in our community...',
];

const COMPLETE_MESSAGE = 'Your hiring edge is ready';
const COMPLETE_MESSAGE = 'Your analysis is ready';

type AnalyzeStatusBarProps = {
loadingStep: number;
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/src/components/recruiter/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export interface RecruiterHeaderProps {
}

export const RecruiterHeader = ({
title = 'Your potential reach',
subtitle = "See how many developers match your role and what they're interested in.",
title = 'Reach analysis',
subtitle = 'See who in our community fits your role',
headerButton,
}: RecruiterHeaderProps) => {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,20 @@ import {
import { useOpportunityPreviewContext } from '../../context/OpportunityPreviewContext';
import { OpportunityPreviewStatus } from '../../types';
import { JobInfo } from './JobInfo';
import { apiUrl } from '../../../../lib/config';
import { ReachHeroSection } from './ReachHeroSection';
import { InsightCard } from './InsightCard';
import { Chip } from '../../../../components/cards/common/PostTags';
import { PlusUserIcon } from '../../../../components/icons/PlusUser';
import { IconSize } from '../../../../components/Icon';

type AnalyzeContentProps = {
loadingStep: number;
};

const iconSize = 24;

export const AnalyzeContent = ({ loadingStep }: AnalyzeContentProps) => {
const data = useOpportunityPreviewContext();
const isReady = data?.result?.status === OpportunityPreviewStatus.READY;
const totalCount = data?.result?.totalCount ?? 0;
const tags = data?.result?.tags ?? [];
const companies = data?.result?.companies ?? [];

// Mock engagement stat - in production this would come from the API
const avgTimePerWeek = '4.2 hrs';

const isError = data?.result?.status === OpportunityPreviewStatus.ERROR;
const showAggregation =
!isError && loadingStep >= 2 && (isReady || tags.length > 0);
const showReachHero = !isError && loadingStep >= 2;

return (
Expand Down Expand Up @@ -60,71 +48,6 @@ export const AnalyzeContent = ({ loadingStep }: AnalyzeContentProps) => {
</div>
)}

{/* Candidate Insights */}
{showAggregation && (
<div className="grid gap-4 tablet:grid-cols-3">
{/* Tags */}
<InsightCard
label="Interested in"
tooltip="Topics these developers actively read and engage with on daily.dev"
isLoading={!isReady}
>
<div className="flex flex-wrap gap-1.5">
{tags.map((tag) => (
<Chip key={tag} className="!my-0">
#{tag}
</Chip>
))}
</div>
</InsightCard>

{/* Companies */}
<InsightCard
label="Currently working at"
tooltip="Companies where matched developers currently work"
isLoading={!isReady}
>
<div className="flex flex-wrap gap-1.5">
{companies.slice(0, 4).map((company) => (
<Chip key={company.name} className="!my-0 gap-1.5">
<img
src={`${apiUrl}/icon?url=${encodeURIComponent(
company.favicon || '',
)}&size=${iconSize}`}
className="size-4 rounded-full bg-surface-float object-contain"
alt={company.name}
/>
<span>{company.name}</span>
</Chip>
))}
</div>
</InsightCard>

{/* Platform Engagement */}
<InsightCard
label="Weekly active time"
tooltip="How much time these developers spend on daily.dev each week"
isLoading={!isReady}
>
<div className="flex flex-col">
<Typography
type={TypographyType.Title2}
bold
className="text-accent-cabbage-default"
>
{avgTimePerWeek}
</Typography>
<Typography
type={TypographyType.Caption1}
color={TypographyColor.Tertiary}
>
avg. per candidate
</Typography>
</div>
</InsightCard>
</div>
)}

{/* Job Summary */}
<div className="rounded-16 border border-border-subtlest-tertiary bg-background-default p-4">
<Typography
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,38 @@ import {
TypographyType,
} from '../../../../components/typography/Typography';
import { useOpportunityPreviewContext } from '../../context/OpportunityPreviewContext';
import { SeniorityLevel } from '../../protobuf/opportunity';
import { SeniorityLevel, SalaryPeriod } from '../../protobuf/opportunity';
import { LocationType } from '../../protobuf/util';
import { seniorityLevelMap } from '../../common';
import { ElementPlaceholder } from '../../../../components/ElementPlaceholder';
import { Chip } from '../../../../components/cards/common/PostTags';
import type { Salary } from '../../types';

const locationTypeMap: Record<LocationType, string | null> = {
[LocationType.UNSPECIFIED]: null,
[LocationType.REMOTE]: 'Remote',
[LocationType.OFFICE]: 'On-site',
[LocationType.HYBRID]: 'Hybrid',
};

const salaryPeriodMap: Record<SalaryPeriod, string> = {
[SalaryPeriod.UNSPECIFIED]: 'year',
[SalaryPeriod.ANNUAL]: 'year',
[SalaryPeriod.MONTHLY]: 'month',
[SalaryPeriod.WEEKLY]: 'week',
[SalaryPeriod.DAILY]: 'day',
[SalaryPeriod.HOURLY]: 'hour',
};

const formatSalary = (salary?: Salary): string | null => {
if (!salary?.min || !salary?.max) {
return null;
}
const min = salary.min / 1000;
const max = salary.max / 1000;
const period = salaryPeriodMap[salary.period ?? SalaryPeriod.UNSPECIFIED];
return `$${min}k - $${max}k/${period}`;
};

type JobInfoProps = {
loadingStep: number;
Expand All @@ -33,8 +61,10 @@ export const JobInfo = ({ loadingStep }: JobInfoProps) => {
);
}

const { locations, meta, title, tldr, keywords } = opportunity;

const locationString =
opportunity.locations
locations
?.map((item) =>
[
item.location?.city,
Expand All @@ -44,51 +74,68 @@ export const JobInfo = ({ loadingStep }: JobInfoProps) => {
.filter(Boolean)
.join(', '),
)
.join(' · ') || 'Location not specified';
.join(' · ') || null;

const seniorityLabel =
seniorityLevelMap[
opportunity.meta?.seniorityLevel ?? SeniorityLevel.UNSPECIFIED
];
seniorityLevelMap[meta?.seniorityLevel ?? SeniorityLevel.UNSPECIFIED];

const workArrangement =
locationTypeMap[locations?.[0]?.type ?? LocationType.UNSPECIFIED];

const salaryRange = formatSalary(meta?.salary);

// Build meta items array for clean rendering
const metaItems = [
locationString,
seniorityLabel !== 'N/A' ? seniorityLabel : null,
workArrangement,
salaryRange,
].filter(Boolean);

return (
<div className="flex flex-col gap-4">
{/* Title and meta */}
<div>
<Typography type={TypographyType.Title3} bold>
{opportunity.title}
{title}
</Typography>
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1">
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
>
{locationString}
</Typography>
{seniorityLabel && (
<>
<span className="text-text-quaternary">·</span>
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
>
{seniorityLabel}
</Typography>
</>
)}
</div>
{metaItems.length > 0 && (
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1">
{metaItems.map((item, index) => (
<React.Fragment key={item}>
{index > 0 && <span className="text-text-quaternary">·</span>}
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
>
{item}
</Typography>
</React.Fragment>
))}
</div>
)}
</div>

{/* TLDR */}
{tldr && (
<Typography
type={TypographyType.Body}
color={TypographyColor.Secondary}
>
<span className="font-bold text-text-primary">TLDR</span> {tldr}
</Typography>
)}

{/* Tech stack */}
{opportunity.keywords && opportunity.keywords.length > 0 && (
{keywords && keywords.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{opportunity.keywords.slice(0, 10).map((tag) => (
{keywords.slice(0, 10).map((tag) => (
<Chip key={tag.keyword} className="!my-0">
#{tag.keyword}
</Chip>
))}
{opportunity.keywords.length > 10 && (
<Chip className="!my-0">+{opportunity.keywords.length - 10}</Chip>
{keywords.length > 10 && (
<Chip className="!my-0">+{keywords.length - 10}</Chip>
)}
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ type ReachHeroSectionProps = {
isLoading: boolean;
};

const PASSIVE_PERCENTAGE = 30;

export const ReachHeroSection = ({
totalCount,
isLoading,
Expand Down Expand Up @@ -58,29 +56,32 @@ export const ReachHeroSection = ({
return (
<div className="rounded-16 border border-border-subtlest-tertiary bg-background-default p-6">
<div className="flex flex-col items-center">
{/* Label */}
<Typography
type={TypographyType.Body}
color={TypographyColor.Secondary}
>
Potential reach
</Typography>
{/* Hero number */}
<Typography type={TypographyType.Tera} bold className="tabular-nums">
{animatedCount.toLocaleString()}
</Typography>
<Typography
type={TypographyType.Title3}
color={TypographyColor.Primary}
className="mt-1"
>
developers matched
</Typography>

{/* Exclusive stat */}
{/* Community differentiator */}
<div className="mt-5 flex animate-fade-slide-up items-center gap-2 rounded-10 bg-surface-float px-3 py-2">
<div className="size-2 animate-pulse rounded-full bg-status-success" />
<Typography
type={TypographyType.Callout}
color={TypographyColor.Secondary}
>
<span className="font-bold text-text-primary">
{PASSIVE_PERCENTAGE}%
</span>{' '}
are passively open to new opportunities
Active in our community
</Typography>
</div>
</div>
Expand Down
Loading