@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.
@@ -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';
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './constants';
3
+ export * from './utils';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Types
2
+ export * from './types';
3
+ // Constants
4
+ export * from './constants';
5
+ // Utils
6
+ export * from './utils';
@@ -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,3 @@
1
+ export * from './job';
2
+ export * from './user';
3
+ export * from './application';
@@ -0,0 +1,3 @@
1
+ export * from './job';
2
+ export * from './user';
3
+ export * from './application';
@@ -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,5 @@
1
+ /**
2
+ * Format experience level for display
3
+ * Converts short codes to user-friendly labels
4
+ */
5
+ export declare function formatExperienceLevel(level: string | null | undefined): string;
@@ -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
+ '&lt;': '<',
6
+ '&gt;': '>',
7
+ '&quot;': '"',
8
+ '&#39;': "'",
9
+ '&amp;': '&',
10
+ '&nbsp;': ' ',
11
+ '&ndash;': '–',
12
+ '&mdash;': '—',
13
+ '&hellip;': '…',
14
+ '&copy;': '©',
15
+ '&reg;': '®',
16
+ '&trade;': '™',
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 (&#123; and &#x1a;)
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,3 @@
1
+ export * from './jobTransformers';
2
+ export * from './htmlCleaner';
3
+ export * from './experienceLevelFormatter';
@@ -0,0 +1,3 @@
1
+ export * from './jobTransformers';
2
+ export * from './htmlCleaner';
3
+ export * from './experienceLevelFormatter';
@@ -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
+ }