@jobloo/shared 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants/app.d.ts +20 -0
- package/dist/constants/app.js +25 -0
- package/dist/constants/companies.d.ts +9 -0
- package/dist/constants/companies.js +19 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/types/application.d.ts +29 -0
- package/dist/types/application.js +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +3 -0
- package/dist/types/job.d.ts +87 -0
- package/dist/types/job.js +1 -0
- package/dist/types/user.d.ts +59 -0
- package/dist/types/user.js +1 -0
- package/dist/utils/experienceLevelFormatter.d.ts +5 -0
- package/dist/utils/experienceLevelFormatter.js +15 -0
- package/dist/utils/htmlCleaner.d.ts +22 -0
- package/dist/utils/htmlCleaner.js +137 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/jobTransformers.d.ts +15 -0
- package/dist/utils/jobTransformers.js +94 -0
- package/package.json +29 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-wide constants
|
|
3
|
+
*/
|
|
4
|
+
export declare const FREE_SWIPES_PER_DAY = 50;
|
|
5
|
+
export declare const PREMIUM_UNLIMITED_SWIPES = -1;
|
|
6
|
+
export declare const BRIGHTDATA_HOST = "brd.superproxy.io";
|
|
7
|
+
export declare const BRIGHTDATA_PORT = 9222;
|
|
8
|
+
export declare const LEVER_API_BASE_URL = "https://api.lever.co/v0/postings";
|
|
9
|
+
export declare const STORAGE_KEYS: {
|
|
10
|
+
readonly USER_DATA: "@jobloo:userData";
|
|
11
|
+
readonly SWIPE_HISTORY: "@jobloo:swipeHistory";
|
|
12
|
+
readonly APPLICATIONS: "@jobloo:applications";
|
|
13
|
+
readonly ONBOARDING_COMPLETED: "@jobloo:onboardingCompleted";
|
|
14
|
+
};
|
|
15
|
+
export declare const BOTTOM_TAB_ROUTES: {
|
|
16
|
+
readonly FEED: "Feed";
|
|
17
|
+
readonly FEEDBACK: "Feedback";
|
|
18
|
+
readonly PROFILE: "Profile";
|
|
19
|
+
readonly SETTINGS: "Settings";
|
|
20
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-wide constants
|
|
3
|
+
*/
|
|
4
|
+
// Freemium limits
|
|
5
|
+
export const FREE_SWIPES_PER_DAY = 50;
|
|
6
|
+
export const PREMIUM_UNLIMITED_SWIPES = -1;
|
|
7
|
+
// BrightData config
|
|
8
|
+
export const BRIGHTDATA_HOST = 'brd.superproxy.io';
|
|
9
|
+
export const BRIGHTDATA_PORT = 9222;
|
|
10
|
+
// Lever API
|
|
11
|
+
export const LEVER_API_BASE_URL = 'https://api.lever.co/v0/postings';
|
|
12
|
+
// Storage keys
|
|
13
|
+
export const STORAGE_KEYS = {
|
|
14
|
+
USER_DATA: '@jobloo:userData',
|
|
15
|
+
SWIPE_HISTORY: '@jobloo:swipeHistory',
|
|
16
|
+
APPLICATIONS: '@jobloo:applications',
|
|
17
|
+
ONBOARDING_COMPLETED: '@jobloo:onboardingCompleted',
|
|
18
|
+
};
|
|
19
|
+
// Navigation
|
|
20
|
+
export const BOTTOM_TAB_ROUTES = {
|
|
21
|
+
FEED: 'Feed',
|
|
22
|
+
FEEDBACK: 'Feedback',
|
|
23
|
+
PROFILE: 'Profile',
|
|
24
|
+
SETTINGS: 'Settings',
|
|
25
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Companies to fetch jobs from via Lever API
|
|
3
|
+
* Format: company slug used in Lever URL (e.g., jobs.lever.co/{slug})
|
|
4
|
+
*
|
|
5
|
+
* NOTE: Only companies with confirmed public postings
|
|
6
|
+
* Test each at: https://jobs.lever.co/{company}
|
|
7
|
+
*/
|
|
8
|
+
export declare const LEVER_COMPANIES: readonly ["lever", "netflix", "plaid", "playplay", "thumbtack", "shopify", "greenhouse", "twitch", "coursera", "mixpanel"];
|
|
9
|
+
export type LeverCompany = typeof LEVER_COMPANIES[number];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Companies to fetch jobs from via Lever API
|
|
3
|
+
* Format: company slug used in Lever URL (e.g., jobs.lever.co/{slug})
|
|
4
|
+
*
|
|
5
|
+
* NOTE: Only companies with confirmed public postings
|
|
6
|
+
* Test each at: https://jobs.lever.co/{company}
|
|
7
|
+
*/
|
|
8
|
+
export const LEVER_COMPANIES = [
|
|
9
|
+
'lever', // Lever's own jobs - always works
|
|
10
|
+
'netflix', // Netflix jobs
|
|
11
|
+
'plaid', // Plaid jobs
|
|
12
|
+
'playplay', // PlayPlay jobs
|
|
13
|
+
'thumbtack', // Thumbtack jobs
|
|
14
|
+
'shopify', // Shopify jobs
|
|
15
|
+
'greenhouse', // Greenhouse jobs
|
|
16
|
+
'twitch', // Twitch jobs
|
|
17
|
+
'coursera', // Coursera jobs
|
|
18
|
+
'mixpanel', // Mixpanel jobs
|
|
19
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './app';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './app';
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job application status
|
|
3
|
+
*/
|
|
4
|
+
export type ApplicationStatus = 'pending' | 'submitting' | 'submitted' | 'failed' | 'skipped';
|
|
5
|
+
/**
|
|
6
|
+
* Job application record
|
|
7
|
+
*/
|
|
8
|
+
export interface Application {
|
|
9
|
+
id: string;
|
|
10
|
+
userId: string;
|
|
11
|
+
jobId: string;
|
|
12
|
+
jobTitle: string;
|
|
13
|
+
company: string;
|
|
14
|
+
applyUrl: string;
|
|
15
|
+
status: ApplicationStatus;
|
|
16
|
+
swipedAt: string;
|
|
17
|
+
submittedAt?: string;
|
|
18
|
+
errorMessage?: string;
|
|
19
|
+
retryCount: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* BrightData form submission result
|
|
23
|
+
*/
|
|
24
|
+
export interface ApplicationResult {
|
|
25
|
+
success: boolean;
|
|
26
|
+
applicationId?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
screenshot?: string;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job posting from Lever API
|
|
3
|
+
*/
|
|
4
|
+
export interface Job {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
company: string;
|
|
8
|
+
companyLogo?: string;
|
|
9
|
+
location: string;
|
|
10
|
+
city?: string;
|
|
11
|
+
country?: string;
|
|
12
|
+
allLocations?: string[];
|
|
13
|
+
description: string;
|
|
14
|
+
descriptionPlain?: string;
|
|
15
|
+
opening?: string;
|
|
16
|
+
openingPlain?: string;
|
|
17
|
+
descriptionBody?: string;
|
|
18
|
+
descriptionBodyPlain?: string;
|
|
19
|
+
additional?: string;
|
|
20
|
+
additionalPlain?: string;
|
|
21
|
+
lists?: Array<{
|
|
22
|
+
text: string;
|
|
23
|
+
content: string;
|
|
24
|
+
}>;
|
|
25
|
+
applyUrl: string;
|
|
26
|
+
hostedUrl: string;
|
|
27
|
+
tags: string[];
|
|
28
|
+
categories: {
|
|
29
|
+
location?: string;
|
|
30
|
+
team?: string;
|
|
31
|
+
commitment?: string;
|
|
32
|
+
department?: string;
|
|
33
|
+
level?: string;
|
|
34
|
+
};
|
|
35
|
+
salaryRange?: {
|
|
36
|
+
currency: string;
|
|
37
|
+
interval: string;
|
|
38
|
+
min: number;
|
|
39
|
+
max: number;
|
|
40
|
+
};
|
|
41
|
+
salaryDescription?: string;
|
|
42
|
+
salaryDescriptionPlain?: string;
|
|
43
|
+
experienceLevel?: string;
|
|
44
|
+
workplaceType?: 'on-site' | 'remote' | 'hybrid' | 'unspecified';
|
|
45
|
+
commitmentType?: string;
|
|
46
|
+
createdAt: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Raw Lever API response
|
|
50
|
+
*/
|
|
51
|
+
export interface LeverJobResponse {
|
|
52
|
+
id: string;
|
|
53
|
+
text: string;
|
|
54
|
+
categories: {
|
|
55
|
+
location?: string;
|
|
56
|
+
team?: string;
|
|
57
|
+
commitment?: string;
|
|
58
|
+
department?: string;
|
|
59
|
+
level?: string;
|
|
60
|
+
allLocations?: string[];
|
|
61
|
+
};
|
|
62
|
+
country?: string;
|
|
63
|
+
opening?: string;
|
|
64
|
+
openingPlain?: string;
|
|
65
|
+
description: string;
|
|
66
|
+
descriptionPlain?: string;
|
|
67
|
+
descriptionBody?: string;
|
|
68
|
+
descriptionBodyPlain?: string;
|
|
69
|
+
lists?: Array<{
|
|
70
|
+
text: string;
|
|
71
|
+
content: string;
|
|
72
|
+
}>;
|
|
73
|
+
additional?: string;
|
|
74
|
+
additionalPlain?: string;
|
|
75
|
+
hostedUrl: string;
|
|
76
|
+
applyUrl: string;
|
|
77
|
+
workplaceType?: string;
|
|
78
|
+
salaryRange?: {
|
|
79
|
+
currency: string;
|
|
80
|
+
interval: string;
|
|
81
|
+
min: number;
|
|
82
|
+
max: number;
|
|
83
|
+
};
|
|
84
|
+
salaryDescription?: string;
|
|
85
|
+
salaryDescriptionPlain?: string;
|
|
86
|
+
createdAt: number;
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User data collected during onboarding
|
|
3
|
+
*/
|
|
4
|
+
export interface User {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
fullName: string;
|
|
8
|
+
firstName?: string;
|
|
9
|
+
lastName?: string;
|
|
10
|
+
phone: string;
|
|
11
|
+
gender?: 'male' | 'female' | 'other' | 'prefer-not-to-say';
|
|
12
|
+
race?: 'asian' | 'black-african-american' | 'white-caucasian' | 'hispanic' | 'native-american-pacific-islander' | 'other' | 'prefer-not-to-say';
|
|
13
|
+
isProtectedVeteran?: 'yes' | 'no' | 'prefer-not-to-say';
|
|
14
|
+
hasDisabilities?: 'yes' | 'no' | 'prefer-not-to-say';
|
|
15
|
+
age?: number;
|
|
16
|
+
referralSource?: 'instagram' | 'twitter' | 'chatgpt' | 'linkedin' | 'reddit' | 'friend' | 'other';
|
|
17
|
+
notificationsEnabled?: boolean;
|
|
18
|
+
emailOptIn?: boolean;
|
|
19
|
+
currentLocation?: string;
|
|
20
|
+
city?: string;
|
|
21
|
+
country?: string;
|
|
22
|
+
useCurrentLocation?: boolean;
|
|
23
|
+
currentCompany?: string;
|
|
24
|
+
linkedinUrl?: string;
|
|
25
|
+
githubUrl?: string;
|
|
26
|
+
portfolioUrl?: string;
|
|
27
|
+
otherWebsite?: string;
|
|
28
|
+
resumeUri?: string;
|
|
29
|
+
resumeFileName?: string;
|
|
30
|
+
cvFileName?: string;
|
|
31
|
+
googleDriveResumeLink?: string;
|
|
32
|
+
jobPreferences: {
|
|
33
|
+
titles: string[];
|
|
34
|
+
locations: string[];
|
|
35
|
+
remotePreference: 'remote' | 'hybrid' | 'on-site' | 'any';
|
|
36
|
+
jobTypes: string[];
|
|
37
|
+
experienceLevels?: string[];
|
|
38
|
+
languages?: string[];
|
|
39
|
+
minSalary?: number;
|
|
40
|
+
maxSalary?: number;
|
|
41
|
+
salaryCurrency?: string;
|
|
42
|
+
};
|
|
43
|
+
authorizedCountries: Array<{
|
|
44
|
+
country: string;
|
|
45
|
+
authorizationStatus: 'citizen' | 'authorized' | 'need-sponsor';
|
|
46
|
+
}>;
|
|
47
|
+
onboardingCompleted: boolean;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
updatedAt: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Minimal user data for initial creation
|
|
53
|
+
*/
|
|
54
|
+
export interface CreateUserInput {
|
|
55
|
+
email: string;
|
|
56
|
+
fullName: string;
|
|
57
|
+
phone: string;
|
|
58
|
+
linkedinUrl: string;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format experience level for display
|
|
3
|
+
* Converts short codes to user-friendly labels
|
|
4
|
+
*/
|
|
5
|
+
export function formatExperienceLevel(level) {
|
|
6
|
+
if (!level)
|
|
7
|
+
return '';
|
|
8
|
+
const formatted = {
|
|
9
|
+
'Entry': 'Entry Level',
|
|
10
|
+
'Mid': 'Mid-Level',
|
|
11
|
+
'Senior': 'Senior Level',
|
|
12
|
+
'Exec': 'Executive',
|
|
13
|
+
};
|
|
14
|
+
return formatted[level] || level;
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode HTML entities and strip HTML tags
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Clean and normalize text content from HTML
|
|
6
|
+
*/
|
|
7
|
+
export declare function cleanHTMLContent(html: string | null | undefined): string;
|
|
8
|
+
/**
|
|
9
|
+
* Get the best description from job fields
|
|
10
|
+
* Handles different ATS structures (Lever, Greenhouse, SmartRecruiters)
|
|
11
|
+
*/
|
|
12
|
+
export declare function getBestDescription(job: {
|
|
13
|
+
source?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
company?: string;
|
|
16
|
+
openingPlain?: string | null;
|
|
17
|
+
descriptionPlain?: string | null;
|
|
18
|
+
descriptionBodyPlain?: string | null;
|
|
19
|
+
opening?: string | null;
|
|
20
|
+
description?: string | null;
|
|
21
|
+
descriptionBody?: string | null;
|
|
22
|
+
}): string;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode HTML entities and strip HTML tags
|
|
3
|
+
*/
|
|
4
|
+
const HTML_ENTITIES = {
|
|
5
|
+
'<': '<',
|
|
6
|
+
'>': '>',
|
|
7
|
+
'"': '"',
|
|
8
|
+
''': "'",
|
|
9
|
+
'&': '&',
|
|
10
|
+
' ': ' ',
|
|
11
|
+
'–': '–',
|
|
12
|
+
'—': '—',
|
|
13
|
+
'…': '…',
|
|
14
|
+
'©': '©',
|
|
15
|
+
'®': '®',
|
|
16
|
+
'™': '™',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Decode HTML entities
|
|
20
|
+
*/
|
|
21
|
+
function decodeHTMLEntities(text) {
|
|
22
|
+
let decoded = text;
|
|
23
|
+
// Decode named entities
|
|
24
|
+
for (const [entity, char] of Object.entries(HTML_ENTITIES)) {
|
|
25
|
+
decoded = decoded.replace(new RegExp(entity, 'g'), char);
|
|
26
|
+
}
|
|
27
|
+
// Decode numeric entities ({ and )
|
|
28
|
+
decoded = decoded.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)));
|
|
29
|
+
decoded = decoded.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
30
|
+
return decoded;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Strip HTML tags from text
|
|
34
|
+
*/
|
|
35
|
+
function stripHTMLTags(html) {
|
|
36
|
+
return html
|
|
37
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') // Remove scripts
|
|
38
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '') // Remove styles
|
|
39
|
+
.replace(/<br\s*\/?>/gi, '\n') // Convert <br> to newlines
|
|
40
|
+
.replace(/<\/p>/gi, '\n\n') // Convert </p> to double newlines
|
|
41
|
+
.replace(/<\/div>/gi, '\n') // Convert </div> to newlines
|
|
42
|
+
.replace(/<\/h[1-6]>/gi, '\n\n') // Convert </h1-6> to double newlines
|
|
43
|
+
.replace(/<\/li>/gi, '\n') // Convert </li> to newlines
|
|
44
|
+
.replace(/<li[^>]*>/gi, '\n• ') // Convert <li> to bullets on NEW LINE
|
|
45
|
+
.replace(/<ul[^>]*>/gi, '\n') // Add newline before lists
|
|
46
|
+
.replace(/<ol[^>]*>/gi, '\n') // Add newline before ordered lists
|
|
47
|
+
.replace(/<[^>]+>/g, ' '); // Remove all other tags
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Clean and normalize text content from HTML
|
|
51
|
+
*/
|
|
52
|
+
export function cleanHTMLContent(html) {
|
|
53
|
+
if (!html)
|
|
54
|
+
return '';
|
|
55
|
+
// First decode HTML entities (important for Greenhouse jobs)
|
|
56
|
+
let cleaned = decodeHTMLEntities(html);
|
|
57
|
+
// Then strip HTML tags
|
|
58
|
+
cleaned = stripHTMLTags(cleaned);
|
|
59
|
+
// Clean up formatting issues
|
|
60
|
+
cleaned = cleaned
|
|
61
|
+
.replace(/\s*:\s*•/g, '') // Remove ": •" patterns
|
|
62
|
+
.replace(/•\s*:/g, '•') // Remove "• :" patterns
|
|
63
|
+
.replace(/•\s*\n\s*•/g, '• ') // Remove empty bullet points
|
|
64
|
+
.replace(/\n{3,}/g, '\n\n') // Max 2 consecutive line breaks
|
|
65
|
+
.replace(/\s+/g, ' ') // Normalize spaces
|
|
66
|
+
.replace(/\n /g, '\n') // Remove spaces after newlines
|
|
67
|
+
.trim();
|
|
68
|
+
return cleaned;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Escape special characters for RegExp
|
|
72
|
+
*/
|
|
73
|
+
function escapeRegExp(string) {
|
|
74
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Detect if description is a fake SmartRecruiters placeholder
|
|
78
|
+
*/
|
|
79
|
+
function isFakeDescription(text, title, company) {
|
|
80
|
+
if (!text || text.trim().length < 100) {
|
|
81
|
+
// Check if it's just "{title} at {company}"
|
|
82
|
+
const safeTitle = title ? escapeRegExp(title) : '';
|
|
83
|
+
const safeCompany = company ? escapeRegExp(company) : '';
|
|
84
|
+
const pattern = new RegExp(`^${safeTitle}\\s+at\\s+${safeCompany}`, 'i');
|
|
85
|
+
if (pattern.test(text.trim())) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get the best description from job fields
|
|
93
|
+
* Handles different ATS structures (Lever, Greenhouse, SmartRecruiters)
|
|
94
|
+
*/
|
|
95
|
+
export function getBestDescription(job) {
|
|
96
|
+
// For LEVER: Use opening (company description) + description_body (role details)
|
|
97
|
+
if (job.source === 'lever') {
|
|
98
|
+
const opening = job.openingPlain || job.opening;
|
|
99
|
+
const body = job.descriptionBodyPlain || job.descriptionBody;
|
|
100
|
+
if (opening && body) {
|
|
101
|
+
return cleanHTMLContent(opening) + '\n\n' + cleanHTMLContent(body);
|
|
102
|
+
}
|
|
103
|
+
else if (body) {
|
|
104
|
+
return cleanHTMLContent(body);
|
|
105
|
+
}
|
|
106
|
+
else if (opening) {
|
|
107
|
+
return cleanHTMLContent(opening);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// For GREENHOUSE: Use the single description field
|
|
111
|
+
if (job.source === 'greenhouse') {
|
|
112
|
+
const desc = job.descriptionPlain || job.description;
|
|
113
|
+
if (desc && desc.trim().length >= 50) {
|
|
114
|
+
return cleanHTMLContent(desc);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// For SMARTRECRUITERS: Check if it's a fake description
|
|
118
|
+
if (job.source === 'smartrecruiters') {
|
|
119
|
+
const desc = job.descriptionPlain || job.description;
|
|
120
|
+
if (desc && isFakeDescription(desc, job.title, job.company)) {
|
|
121
|
+
return ''; // Return empty for fake descriptions
|
|
122
|
+
}
|
|
123
|
+
if (desc && desc.trim().length >= 50) {
|
|
124
|
+
return cleanHTMLContent(desc);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Fallback for unknown sources or if source-specific logic failed
|
|
128
|
+
const plainText = job.openingPlain || job.descriptionPlain || job.descriptionBodyPlain;
|
|
129
|
+
if (plainText && plainText.trim().length >= 50) {
|
|
130
|
+
return cleanHTMLContent(plainText);
|
|
131
|
+
}
|
|
132
|
+
const htmlText = job.opening || job.description || job.descriptionBody;
|
|
133
|
+
if (htmlText && htmlText.trim().length >= 50) {
|
|
134
|
+
return cleanHTMLContent(htmlText);
|
|
135
|
+
}
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Job, LeverJobResponse } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Transform Lever API response to our Job type
|
|
4
|
+
*/
|
|
5
|
+
export declare function transformLeverJob(leverJob: LeverJobResponse, company: string): Job;
|
|
6
|
+
/**
|
|
7
|
+
* Format salary range for detailed display
|
|
8
|
+
* Example: "$50,000 - $70,000 per year"
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatSalaryRange(salaryRange: {
|
|
11
|
+
currency: string;
|
|
12
|
+
interval: string;
|
|
13
|
+
min: number;
|
|
14
|
+
max: number;
|
|
15
|
+
}): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Lever API response to our Job type
|
|
3
|
+
*/
|
|
4
|
+
export function transformLeverJob(leverJob, company) {
|
|
5
|
+
// Build tags from various fields
|
|
6
|
+
const tags = [];
|
|
7
|
+
if (leverJob.categories.commitment)
|
|
8
|
+
tags.push(leverJob.categories.commitment);
|
|
9
|
+
if (leverJob.categories.level)
|
|
10
|
+
tags.push(leverJob.categories.level);
|
|
11
|
+
if (leverJob.workplaceType && leverJob.workplaceType !== 'unspecified') {
|
|
12
|
+
tags.push(leverJob.workplaceType);
|
|
13
|
+
}
|
|
14
|
+
if (leverJob.salaryRange) {
|
|
15
|
+
tags.push(formatSalaryRangeTag(leverJob.salaryRange));
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
id: leverJob.id,
|
|
19
|
+
title: leverJob.text,
|
|
20
|
+
company,
|
|
21
|
+
location: leverJob.categories.location || 'Remote',
|
|
22
|
+
allLocations: leverJob.categories.allLocations,
|
|
23
|
+
country: leverJob.country,
|
|
24
|
+
description: leverJob.description,
|
|
25
|
+
descriptionPlain: leverJob.descriptionPlain,
|
|
26
|
+
opening: leverJob.opening,
|
|
27
|
+
openingPlain: leverJob.openingPlain,
|
|
28
|
+
descriptionBody: leverJob.descriptionBody,
|
|
29
|
+
descriptionBodyPlain: leverJob.descriptionBodyPlain,
|
|
30
|
+
additional: leverJob.additional,
|
|
31
|
+
additionalPlain: leverJob.additionalPlain,
|
|
32
|
+
lists: leverJob.lists,
|
|
33
|
+
applyUrl: leverJob.applyUrl,
|
|
34
|
+
hostedUrl: leverJob.hostedUrl,
|
|
35
|
+
categories: {
|
|
36
|
+
location: leverJob.categories.location,
|
|
37
|
+
team: leverJob.categories.team,
|
|
38
|
+
commitment: leverJob.categories.commitment,
|
|
39
|
+
department: leverJob.categories.department,
|
|
40
|
+
level: leverJob.categories.level,
|
|
41
|
+
},
|
|
42
|
+
tags,
|
|
43
|
+
salaryRange: leverJob.salaryRange,
|
|
44
|
+
salaryDescription: leverJob.salaryDescription,
|
|
45
|
+
salaryDescriptionPlain: leverJob.salaryDescriptionPlain,
|
|
46
|
+
workplaceType: leverJob.workplaceType || 'unspecified',
|
|
47
|
+
createdAt: leverJob.createdAt,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Format salary range for display tag
|
|
52
|
+
* Converts $54232-$72719 to "$50k - $70k"
|
|
53
|
+
*/
|
|
54
|
+
function formatSalaryRangeTag(salaryRange) {
|
|
55
|
+
const formatAmount = (amount) => {
|
|
56
|
+
if (amount >= 1000000) {
|
|
57
|
+
return `${Math.round(amount / 100000) / 10}M`;
|
|
58
|
+
}
|
|
59
|
+
if (amount >= 1000) {
|
|
60
|
+
return `${Math.round(amount / 1000)}k`;
|
|
61
|
+
}
|
|
62
|
+
return amount.toString();
|
|
63
|
+
};
|
|
64
|
+
const currencySymbol = getCurrencySymbol(salaryRange.currency);
|
|
65
|
+
return `${currencySymbol}${formatAmount(salaryRange.min)} - ${currencySymbol}${formatAmount(salaryRange.max)}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get currency symbol from currency code
|
|
69
|
+
*/
|
|
70
|
+
function getCurrencySymbol(currencyCode) {
|
|
71
|
+
const symbols = {
|
|
72
|
+
'USD': '$',
|
|
73
|
+
'EUR': '€',
|
|
74
|
+
'GBP': '£',
|
|
75
|
+
'JPY': '¥',
|
|
76
|
+
'CAD': 'C$',
|
|
77
|
+
'AUD': 'A$',
|
|
78
|
+
'CHF': 'Fr',
|
|
79
|
+
'CNY': '¥',
|
|
80
|
+
'INR': '₹',
|
|
81
|
+
};
|
|
82
|
+
return symbols[currencyCode] || currencyCode;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Format salary range for detailed display
|
|
86
|
+
* Example: "$50,000 - $70,000 per year"
|
|
87
|
+
*/
|
|
88
|
+
export function formatSalaryRange(salaryRange) {
|
|
89
|
+
const currencySymbol = getCurrencySymbol(salaryRange.currency);
|
|
90
|
+
const minFormatted = salaryRange.min.toLocaleString();
|
|
91
|
+
const maxFormatted = salaryRange.max.toLocaleString();
|
|
92
|
+
const intervalText = salaryRange.interval ? ` per ${salaryRange.interval}` : '';
|
|
93
|
+
return `${currencySymbol}${minFormatted} - ${currencySymbol}${maxFormatted}${intervalText}`;
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jobloo/shared",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared types and utilities for Jobloo",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"clean": "rm -rf dist",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.9.3"
|
|
17
|
+
},
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/Crimsontg7/jobloo-prod.git",
|
|
22
|
+
"directory": "packages/shared"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"jobloo",
|
|
26
|
+
"types",
|
|
27
|
+
"shared"
|
|
28
|
+
]
|
|
29
|
+
}
|