Function bodies 234 total
HowItWorks function · typescript · L650-L746 (97 LOC)app/(marketing)/page.tsx
function HowItWorks() {
const steps = [
{
number: '1',
title: 'Save jobs instantly',
description: 'Use our Chrome extension to save jobs from LinkedIn, Indeed, or any job board with one click.',
icon: (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
)
},
{
number: '2',
title: 'Everything syncs',
description: 'Connect Gmail and watch your applications update automatically. Interview invites, rejections — all tracked.',
icon: (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
)
},
{
number: '3',
title:Testimonials function · typescript · L751-L836 (86 LOC)app/(marketing)/page.tsx
function Testimonials() {
const testimonials = [
{
quote: "The Chrome extension is a game changer. I save jobs from LinkedIn in one click while browsing, and they're all organized in my board when I'm ready to apply.",
author: "Sarah Chen",
role: "Senior Product Manager",
result: "Hired at Stripe",
avatar: "S"
},
{
quote: "Gmail sync is magic. Applied to 47 jobs and never lost track of a single one. When Spotify called for an interview, I had all my research notes ready.",
author: "Marcus Johnson",
role: "Software Engineer",
result: "Hired at Spotify",
avatar: "M"
},
{
quote: "The AI research chat told me exactly what to expect in my Notion interview. I walked in knowing their values, recent news, and the right questions to ask.",
author: "Priya Patel",
role: "Marketing Director",
result: "3 offers in 4 weeks",
avatar: "P"
}
];
return (
<section className="py-24 CTASection function · typescript · L841-L882 (42 LOC)app/(marketing)/page.tsx
function CTASection() {
return (
<section className="py-24 lg:py-32 bg-gray-900 relative overflow-hidden">
{/* Decorative */}
<div className="absolute top-0 left-0 w-96 h-96 bg-gray-800 rounded-full blur-3xl -translate-x-1/2 -translate-y-1/2" />
<div className="absolute bottom-0 right-0 w-96 h-96 bg-gray-800 rounded-full blur-3xl translate-x-1/2 translate-y-1/2" />
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={fadeInUp}
className="relative z-10 max-w-4xl mx-auto px-6 text-center"
>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-4">
Your next job is out there. Go get it.
</h2>
<p className="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
Track every application, research any company, and generate tailored CVs — all in one place. Free to start.
</p>
<div className="flex flex-col sm:flex-roFooter function · typescript · L887-L962 (76 LOC)app/(marketing)/page.tsx
function Footer() {
return (
<footer className="py-16 bg-white border-t border-gray-100">
<div className="max-w-7xl mx-auto px-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-12">
{/* Brand */}
<div className="col-span-2 md:col-span-1">
<a href="/" className="flex items-center gap-1.5 mb-4">
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none">
<path d="M3 18C3 10 7 4 12 4C17 4 21 10 21 18" stroke="url(#logo-gradient-footer)" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M6 17C6 11 8.5 6.5 12 6.5C15.5 6.5 18 11 18 17" stroke="url(#logo-gradient-footer)" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M9 16C9 12 10.5 9 12 9C13.5 9 15 12 15 16" stroke="url(#logo-gradient-footer)" strokeWidth="1.5" strokeLinecap="round"/>
<defs>
<linearGradient id="logo-gradient-footer" x1="3" y1="4" x2="21" y2="18" gradientUnits="userLandingPage function · typescript · L967-L980 (14 LOC)app/(marketing)/page.tsx
export default function LandingPage() {
return (
<main className="scroll-smooth bg-white">
<Nav />
<HeroSection />
<LogoBar />
<FeatureSections />
<HowItWorks />
<Testimonials />
<CTASection />
<Footer />
</main>
);
}Nav function · typescript · L96-L130 (35 LOC)app/(marketing)/pricing/page.tsx
function Nav() {
return (
<nav className="fixed top-0 left-0 right-0 z-50 px-6 py-4 bg-white/80 backdrop-blur-lg border-b border-gray-100">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<a href="/" className="flex items-center gap-1.5">
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none">
<path d="M3 18C3 10 7 4 12 4C17 4 21 10 21 18" stroke="url(#logo-gradient-pricing-nav)" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M6 17C6 11 8.5 6.5 12 6.5C15.5 6.5 18 11 18 17" stroke="url(#logo-gradient-pricing-nav)" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M9 16C9 12 10.5 9 12 9C13.5 9 15 12 15 16" stroke="url(#logo-gradient-pricing-nav)" strokeWidth="1.5" strokeLinecap="round"/>
<defs>
<linearGradient id="logo-gradient-pricing-nav" x1="3" y1="4" x2="21" y2="18" gradientUnits="userSpaceOnUse">
<stop stopColor="#059669"/>
<stop ofPricingHero function · typescript · L135-L184 (50 LOC)app/(marketing)/pricing/page.tsx
function PricingHero() {
return (
<section className="relative pt-32 pb-16 bg-gradient-to-b from-indigo-50 via-white to-white">
<div className="absolute top-20 left-10 w-72 h-72 bg-indigo-100/50 rounded-full blur-3xl" />
<div className="absolute top-40 right-10 w-96 h-96 bg-blue-50/50 rounded-full blur-3xl" />
<div className="relative z-10 max-w-7xl mx-auto px-6 text-center">
<motion.div initial="hidden" animate="visible" variants={staggerContainer}>
<motion.div variants={fadeInUp}>
<span className="inline-flex items-center gap-2 px-4 py-2 mb-6 rounded-full bg-indigo-100 border border-indigo-200">
<span className="text-sm font-medium text-indigo-700">No subscriptions. Pay once, use forever.</span>
</span>
</motion.div>
<motion.h1 variants={fadeInUp} className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-gray-900 mb-4">
Buy credits.{' '}
<span Want this analysis on your repo? https://repobility.com/scan/
PricingCard function · typescript · L189-L280 (92 LOC)app/(marketing)/pricing/page.tsx
function PricingCard({ tier }: { tier: typeof pricingTiers[0] }) {
const isPopular = tier.popular;
const isFree = tier.price === 0;
return (
<motion.div
variants={fadeInUp}
className={`relative rounded-2xl p-8 ${
isPopular
? 'bg-indigo-600 text-white ring-4 ring-indigo-600 ring-offset-4 ring-offset-white scale-105'
: 'bg-white border border-gray-200'
}`}
>
{tier.badge && (
<span className="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-1 bg-amber-400 text-amber-900 text-xs font-bold rounded-full">
{tier.badge}
</span>
)}
{isPopular && (
<span className="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-1 bg-white text-indigo-600 text-xs font-bold rounded-full">
Most Popular
</span>
)}
<div className="text-center mb-6">
<h3 className={`text-xl font-bold mb-1 ${isPopular ? 'text-white' : 'text-gray-900'}`}>
{tier.name}
PricingCardsSection function · typescript · L282-L324 (43 LOC)app/(marketing)/pricing/page.tsx
function PricingCardsSection() {
return (
<section className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-6">
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={staggerContainer}
className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-4 items-start"
>
{pricingTiers.map((tier) => (
<PricingCard key={tier.name} tier={tier} />
))}
</motion.div>
{/* Value comparison */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="mt-16 text-center"
>
<p className="text-gray-500 mb-4">Cost per tailored CV</p>
<div className="flex flex-wrap justify-center gap-8">
<div className="text-center">
<p className="text-2xl font-bold text-gray-900">$0.90</p>
<WhyCreditsSection function · typescript · L329-L396 (68 LOC)app/(marketing)/pricing/page.tsx
function WhyCreditsSection() {
const reasons = [
{
icon: (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
title: 'Job search is temporary',
description: 'You search for 2-6 months, land a job, then stop. Why pay monthly for something you won\'t use?'
},
{
icon: (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
title: 'No subscription guilt',
description: 'No forgetting to cancel. No paying when you\'re not using it. Buy once, use when you need.'
},
{
icon: (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"FAQSection function · typescript · L401-L462 (62 LOC)app/(marketing)/pricing/page.tsx
function FAQSection() {
const faqs = [
{
q: 'Do credits expire?',
a: 'No! Your credits never expire. Buy them today, use them in 2 years. They\'re yours forever.'
},
{
q: 'What counts as one CV credit?',
a: 'One credit = one tailored CV generated for a specific job. You can download it as many times as you want.'
},
{
q: 'Can I use the free tier forever?',
a: 'Yes! The free tier gives you 3 CVs per month, forever. No credit card required.'
},
{
q: 'What if I run out of credits?',
a: 'Just buy another pack. Your new credits add to any remaining ones.'
},
{
q: 'Is there a refund policy?',
a: 'Yes, 14-day money-back guarantee. If you\'re not happy, we\'ll refund you, no questions asked.'
},
{
q: 'Can I upgrade later?',
a: 'Absolutely. Buy any pack anytime. Credits stack, so you never lose what you have.'
},
];
return (
<section className="py-20 bg-white">
CTASection function · typescript · L467-L497 (31 LOC)app/(marketing)/pricing/page.tsx
function CTASection() {
return (
<section className="py-20 bg-gradient-to-br from-indigo-500 to-indigo-700 relative overflow-hidden">
<div className="absolute top-0 left-0 w-96 h-96 bg-white/10 rounded-full blur-3xl -translate-x-1/2 -translate-y-1/2" />
<div className="absolute bottom-0 right-0 w-96 h-96 bg-indigo-400/20 rounded-full blur-3xl translate-x-1/2 translate-y-1/2" />
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="relative z-10 max-w-4xl mx-auto px-6 text-center"
>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-4">
Start free. Buy credits when you need them.
</h2>
<p className="text-lg text-indigo-100 mb-8 max-w-2xl mx-auto">
Try 3 free CVs. See the magic. Then decide.
</p>
<a
href="/cv-input"
className="inline-flex items-center gap-2 px-8 py-Footer function · typescript · L502-L526 (25 LOC)app/(marketing)/pricing/page.tsx
function Footer() {
return (
<footer className="py-16 bg-white border-t border-gray-100">
<div className="max-w-7xl mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<a href="/" className="flex items-center gap-1.5">
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none">
<path d="M3 18C3 10 7 4 12 4C17 4 21 10 21 18" stroke="url(#logo-gradient-pricing-footer)" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M6 17C6 11 8.5 6.5 12 6.5C15.5 6.5 18 11 18 17" stroke="url(#logo-gradient-pricing-footer)" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M9 16C9 12 10.5 9 12 9C13.5 9 15 12 15 16" stroke="url(#logo-gradient-pricing-footer)" strokeWidth="1.5" strokeLinecap="round"/>
<defs>
<linearGradient id="logo-gradient-pricing-footer" x1="3" y1="4" x2="21" y2="18" gradientUnits="userSpaceOnUse">
<stop stopColor=PricingPage function · typescript · L531-L543 (13 LOC)app/(marketing)/pricing/page.tsx
export default function PricingPage() {
return (
<main className="scroll-smooth bg-white min-h-screen">
<Nav />
<PricingHero />
<PricingCardsSection />
<WhyCreditsSection />
<FAQSection />
<CTASection />
<Footer />
</main>
);
}AddJobPage function · typescript · L15-L203 (189 LOC)app/(onboarding)/add-job/page.tsx
export default function AddJobPage() {
const router = useRouter();
const [jobUrl, setJobUrl] = useState('');
const [company, setCompany] = useState('');
const [role, setRole] = useState('');
const [showManualEntry, setShowManualEntry] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleGenerate = async () => {
setIsLoading(true);
// Save job details to localStorage
localStorage.setItem('onboarding_job', JSON.stringify({
url: jobUrl,
company: company || 'Example Company',
role: role || 'Software Engineer',
}));
// Simulate API call for fetching job details / generating CV
setTimeout(() => {
router.push('/success');
}, 2000);
};
const handleSkip = () => {
localStorage.setItem('onboarding_job', JSON.stringify({
url: '',
company: '',
role: '',
skipped: true,
}));
router.push('/success');
};
const hasInput = jobUrl.trim() || (company.trim() && roleRepobility · code-quality intelligence · https://repobility.com
CVInputPage function · typescript · L29-L193 (165 LOC)app/(onboarding)/cv-input/page.tsx
export default function CVInputPage() {
const router = useRouter();
const fileInputRef = useRef<HTMLInputElement>(null);
// Use lazy initialization to avoid effect-based setState
const [cvText, setCvText] = useState(() => getInitialCVText());
const [isUploading, setIsUploading] = useState(false);
const [fileName, setFileName] = useState<string | null>(null);
// Save CV text to localStorage
const saveCVText = (text: string) => {
setCvText(text);
localStorage.setItem('onboarding_cv_text', text);
localStorage.setItem('onboarding_cv_source', 'paste');
};
const handleContinue = () => {
if (cvText.trim()) {
localStorage.setItem('onboarding_cv_text', cvText);
localStorage.setItem('onboarding_cv_source', 'paste');
}
router.push('/questions');
};
const handleNoCVClick = () => {
localStorage.setItem('onboarding_cv_text', '');
localStorage.setItem('onboarding_cv_source', 'scratch');
router.push('/questions');
};
consOnboardingLayout function · typescript · L19-L82 (64 LOC)app/(onboarding)/layout.tsx
export default function OnboardingLayout({ children }: { children: ReactNode }) {
const pathname = usePathname();
const currentStep = stepMap[pathname] || 1;
// Don't show progress on success page
const showProgress = pathname !== '/success';
return (
<div className="min-h-screen bg-gradient-to-b from-gray-50 via-white to-white">
{/* Subtle decorative elements */}
<div className="fixed top-20 left-10 w-72 h-72 bg-gray-200/50 rounded-full blur-3xl pointer-events-none" />
<div className="fixed bottom-20 right-10 w-96 h-96 bg-gray-100/60 rounded-full blur-3xl pointer-events-none" />
{/* Grid pattern overlay */}
<div
className="fixed inset-0 pointer-events-none"
style={{
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' fill='none' stroke='rgb(148 163 184 / 0.05)'%3e%3cpath d='M0 .5H31.5V32'/%3e%3c/svg%3e")`,
}}
/>
{/* LogetInitialState function · typescript · L62-L78 (17 LOC)app/(onboarding)/questions/page.tsx
function getInitialState() {
if (typeof window === 'undefined') {
return { questions: QUESTIONS_WITH_CV, answers: {} };
}
const cvSource = localStorage.getItem('onboarding_cv_source');
const cvText = localStorage.getItem('onboarding_cv_text');
const savedAnswers = localStorage.getItem('onboarding_answers');
const questions = cvSource === 'scratch' || !cvText || cvText.trim() === ''
? QUESTIONS_NO_CV
: QUESTIONS_WITH_CV;
const answers = savedAnswers ? JSON.parse(savedAnswers) : {};
return { questions, answers };
}QuestionsPage function · typescript · L80-L254 (175 LOC)app/(onboarding)/questions/page.tsx
export default function QuestionsPage() {
const router = useRouter();
// Initialize all state from localStorage to avoid useEffect setState
const initialState = useMemo(() => getInitialState(), []);
const [questions] = useState(initialState.questions);
const [currentIndex, setCurrentIndex] = useState(0);
const [answers, setAnswers] = useState<Record<string, string>>(initialState.answers);
const [currentAnswer, setCurrentAnswer] = useState(() => {
const firstQuestion = initialState.questions[0];
return firstQuestion ? (initialState.answers[firstQuestion.id] || '') : '';
});
const [skipMessage, setSkipMessage] = useState<string | null>(null);
const currentQuestion = questions[currentIndex];
const isLastQuestion = currentIndex === questions.length - 1;
const saveAndProceed = useCallback((includeCurrentAnswer: boolean) => {
const updatedAnswers = { ...answers };
if (includeCurrentAnswer && currentAnswer.trim()) {
updatedAnswers[currentQuesenhanceCV function · typescript · L22-L60 (39 LOC)app/(onboarding)/review/page.tsx
async function enhanceCV() {
try {
// Get stored CV text and answers from localStorage
const cvText = localStorage.getItem('onboarding_cv_text') || '';
const answersJson = localStorage.getItem('onboarding_answers');
const answers = answersJson ? JSON.parse(answersJson) : {};
// Call the enhance-cv API with the user's actual data
const response = await fetch('/api/onboarding/enhance-cv', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
cvText,
answers,
}),
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || 'Failed to enhance CV');
}
setMasterCV(data.masterCV);
} catch (err) {
console.error('Error enhancing CV:', err);
setError(err instanceof Error ? err.message : 'Failed to Confetti function · typescript · L9-L53 (45 LOC)app/(onboarding)/success/page.tsx
function Confetti() {
const colors = ['#8B5CF6', '#A78BFA', '#C4B5FD', '#60A5FA', '#34D399', '#FBBF24'];
const [windowHeight, setWindowHeight] = useState(0);
useEffect(() => {
setWindowHeight(window.innerHeight);
}, []);
if (windowHeight === 0) return null;
return (
<div className="fixed inset-0 pointer-events-none overflow-hidden">
{Array.from({ length: 50 }).map((_, i) => {
// Generate random values for each confetti piece
const leftPos = `${(i * 17 + 7) % 100}%`;
const rotateAmount = ((i * 23) % 720) - 360;
const duration = 2 + (i % 3);
const delay = (i % 10) * 0.05;
return (
<motion.div
key={i}
className="absolute w-3 h-3 rounded-sm"
style={{
backgroundColor: colors[i % colors.length],
left: leftPos,
top: -20,
}}
initial={{ y: -20, rotate: 0, opacity: 1 }}
animate={{
y:SuccessPage function · typescript · L72-L266 (195 LOC)app/(onboarding)/success/page.tsx
export default function SuccessPage() {
const router = useRouter();
const [showConfetti, setShowConfetti] = useState(true);
const [mounted, setMounted] = useState(false);
// Initialize job details on mount
const [jobDetails, setJobDetails] = useState<{ company: string; role: string } | null>(null);
useEffect(() => {
setMounted(true);
setJobDetails(getInitialJobDetails());
// Hide confetti after animation
const timer = setTimeout(() => setShowConfetti(false), 4000);
return () => clearTimeout(timer);
}, []);
const handleDownload = () => {
// Placeholder for PDF download functionality
alert('PDF download will be implemented with the API integration.');
};
const handleGoToDashboard = () => {
// Mark onboarding as complete
localStorage.setItem('onboarding_completed', 'true');
router.push('/dashboard');
};
return (
<>
{mounted && showConfetti && <Confetti />}
<OnboardingCard className="text-center">
updateBadge function · javascript · L20-L35 (16 LOC)extension/background.js
async function updateBadge() {
try {
const { authToken } = await chrome.storage.local.get(['authToken']);
if (authToken) {
// User is logged in - clear badge
chrome.action.setBadgeText({ text: '' });
} else {
// User needs to log in - show indicator
chrome.action.setBadgeText({ text: '!' });
chrome.action.setBadgeBackgroundColor({ color: BADGE_COLORS.error });
}
} catch (e) {
console.error('Error updating badge:', e);
}
}If a scraper extracted this row, it came from Repobility (https://repobility.com)
checkDuplicate function · javascript · L45-L74 (30 LOC)extension/background.js
async function checkDuplicate(company, role) {
try {
const { authToken, userId } = await chrome.storage.local.get(['authToken', 'userId']);
if (!authToken || !userId) {
return { isDuplicate: false };
}
const response = await fetch(
`${API_BASE_URL}/api/applications/check-duplicate?` +
new URLSearchParams({ company, role, user_id: userId }),
{
headers: {
'Authorization': `Bearer ${authToken}`,
},
}
);
if (!response.ok) {
return { isDuplicate: false };
}
const data = await response.json();
return { isDuplicate: data.exists, existingJob: data.job };
} catch (e) {
console.error('Error checking duplicate:', e);
return { isDuplicate: false };
}
}extractGeneric function · javascript · L150-L166 (17 LOC)extension/content.js
function extractGeneric() {
const data = {};
// Try to get title from h1 or page title
const h1 = document.querySelector('h1');
if (h1) {
data.role = h1.textContent.trim();
}
// Try to get company from meta tags or common elements
const metaCompany = document.querySelector('meta[property="og:site_name"]');
if (metaCompany) {
data.company = metaCompany.content;
}
return data;
}extractJobData function · javascript · L169-L183 (15 LOC)extension/content.js
function extractJobData() {
const url = window.location.href;
// Find matching extractor
for (const [name, extractor] of Object.entries(extractors)) {
if (extractor.match(url)) {
console.log(`Using ${name} extractor`);
return extractor.extract();
}
}
// Fall back to generic extraction
console.log('Using generic extractor');
return extractGeneric();
}extractJobDataFromPage function · javascript · L16-L151 (136 LOC)extension/popup.js
function extractJobDataFromPage() {
const url = window.location.href;
const data = {};
// LinkedIn
if (url.includes('linkedin.com')) {
const companyEl = document.querySelector('.job-details-jobs-unified-top-card__company-name a') ||
document.querySelector('.jobs-unified-top-card__company-name a') ||
document.querySelector('[data-tracking-control-name="public_jobs_topcard-org-name"]') ||
document.querySelector('.topcard__org-name-link') ||
document.querySelector('.job-details-jobs-unified-top-card__company-name');
if (companyEl) data.company = companyEl.textContent.trim();
const titleEl = document.querySelector('.job-details-jobs-unified-top-card__job-title h1') ||
document.querySelector('.jobs-unified-top-card__job-title') ||
document.querySelector('.topcard__title') ||
document.querySelector('h1.t-24');
if (titleEcheckDuplicate function · javascript · L197-L216 (20 LOC)extension/popup.js
async function checkDuplicate(url) {
try {
const response = await fetch(
`${API_BASE_URL}/api/extension/check-duplicate?url=${encodeURIComponent(url)}`
);
if (response.ok) {
const data = await response.json();
if (data.isDuplicate) {
const app = data.existingApplication;
showStatus(
`Already saved: ${app.company} - ${app.role} (${app.status})`,
'warning'
);
}
}
} catch (e) {
console.log('Could not check for duplicates');
}
}getUserBillingInfo function · typescript · L42-L84 (43 LOC)lib/billing.ts
export async function getUserBillingInfo(userId: string): Promise<UserBillingInfo | null> {
const supabase = createServerClient();
if (!supabase) {
console.error('Supabase not configured');
return null;
}
// Try to get existing billing record
const { data, error } = await supabase
.from('user_billing')
.select('*')
.eq('user_id', userId)
.single();
if (error && error.code !== 'PGRST116') {
// PGRST116 = not found
console.error('Error fetching billing info:', error);
return null;
}
// If no record exists, create default free tier
if (!data) {
const { data: newData, error: insertError } = await supabase
.from('user_billing')
.insert({
user_id: userId,
subscription_tier: 'free',
cv_generation_limit: 3,
cv_generations_used: 0,
})
.select()
.single();
if (insertError) {
console.error('Error creating billing record:', insertError);
return null;
}
formatBillingInfo function · typescript · L89-L106 (18 LOC)lib/billing.ts
function formatBillingInfo(data: Record<string, unknown>): UserBillingInfo {
const tier = (data.subscription_tier as SubscriptionTier) || 'free';
const cvUsed = (data.cv_generations_used as number) || 0;
const cvLimit = (data.cv_generation_limit as number) || 3;
const isUnlimited = tier === 'power';
return {
userId: data.user_id as string,
tier,
cvUsed,
cvLimit: isUnlimited ? -1 : cvLimit,
cvRemaining: isUnlimited ? -1 : Math.max(0, cvLimit - cvUsed),
billingCycleStart: data.billing_cycle_start as string,
subscriptionEndsAt: (data.subscription_ends_at as string) || null,
stripeCustomerId: (data.stripe_customer_id as string) || null,
stripeSubscriptionId: (data.stripe_subscription_id as string) || null,
};
}canGenerateCV function · typescript · L111-L157 (47 LOC)lib/billing.ts
export async function canGenerateCV(userId: string): Promise<CanGenerateCVResult> {
const supabase = createServerClient();
if (!supabase) {
return {
canGenerate: false,
reason: 'error',
used: 0,
limit: 0,
tier: 'free',
};
}
// Use the database function for atomic check
const { data, error } = await supabase.rpc('check_can_generate_cv', {
p_user_id: userId,
});
if (error) {
console.error('Error checking CV generation:', error);
return {
canGenerate: false,
reason: 'error',
used: 0,
limit: 0,
tier: 'free',
};
}
const result = data?.[0];
if (!result) {
return {
canGenerate: false,
reason: 'error',
used: 0,
limit: 0,
tier: 'free',
};
}
return {
canGenerate: result.can_generate,
reason: result.reason as CanGenerateCVResult['reason'],
used: result.used,
limit: result.limit_val,
tier: result.tier as SubscriptionTier,
};
}Repobility · open methodology · https://repobility.com/research/
upgradeTier function · typescript · L209-L254 (46 LOC)lib/billing.ts
export async function upgradeTier(
userId: string,
tier: SubscriptionTier,
stripeCustomerId?: string,
stripeSubscriptionId?: string,
subscriptionEndsAt?: Date
): Promise<boolean> {
const supabase = createServerClient();
if (!supabase) {
return false;
}
const limit = TIER_LIMITS[tier].cvLimit;
const { error } = await supabase
.from('user_billing')
.upsert({
user_id: userId,
subscription_tier: tier,
cv_generation_limit: limit === -1 ? 999999 : limit, // Store large number for unlimited
stripe_customer_id: stripeCustomerId,
stripe_subscription_id: stripeSubscriptionId,
subscription_ends_at: subscriptionEndsAt?.toISOString(),
billing_cycle_start: new Date().toISOString(),
cv_generations_used: 0, // Reset on upgrade
})
.eq('user_id', userId);
if (error) {
console.error('Error upgrading tier:', error);
return false;
}
// Log the billing event
await supabase.from('billing_events').inserdowngradeTier function · typescript · L259-L288 (30 LOC)lib/billing.ts
export async function downgradeTier(userId: string): Promise<boolean> {
const supabase = createServerClient();
if (!supabase) {
return false;
}
const { error } = await supabase
.from('user_billing')
.update({
subscription_tier: 'free',
cv_generation_limit: TIER_LIMITS.free.cvLimit,
stripe_subscription_id: null,
subscription_ends_at: null,
})
.eq('user_id', userId);
if (error) {
console.error('Error downgrading tier:', error);
return false;
}
// Log the billing event
await supabase.from('billing_events').insert({
user_id: userId,
event_type: 'subscription_cancelled',
metadata: {},
});
return true;
}resetMonthlyUsage function · typescript · L293-L307 (15 LOC)lib/billing.ts
export async function resetMonthlyUsage(): Promise<number> {
const supabase = createServerClient();
if (!supabase) {
return 0;
}
const { data, error } = await supabase.rpc('reset_monthly_usage');
if (error) {
console.error('Error resetting monthly usage:', error);
return 0;
}
return data || 0;
}recordBillingEvent function · typescript · L312-L340 (29 LOC)lib/billing.ts
export async function recordBillingEvent(
userId: string,
eventType: string,
stripeEventId: string,
amountCents?: number,
currency?: string,
metadata?: Record<string, unknown>
): Promise<boolean> {
const supabase = createServerClient();
if (!supabase) {
return false;
}
const { error } = await supabase.from('billing_events').insert({
user_id: userId,
event_type: eventType,
stripe_event_id: stripeEventId,
amount_cents: amountCents,
currency: currency || 'usd',
metadata: metadata || {},
});
if (error) {
console.error('Error recording billing event:', error);
return false;
}
return true;
}getBillingHistory function · typescript · L345-L354 (10 LOC)lib/billing.ts
export async function getBillingHistory(
userId: string,
limit: number = 10
): Promise<Array<{
id: string;
eventType: string;
amountCents: number | null;
currency: string;
createdAt: string;
}>> {findUserByStripeCustomerId function · typescript · L384-L404 (21 LOC)lib/billing.ts
export async function findUserByStripeCustomerId(
stripeCustomerId: string
): Promise<string | null> {
const supabase = createServerClient();
if (!supabase) {
return null;
}
const { data, error } = await supabase
.from('user_billing')
.select('user_id')
.eq('stripe_customer_id', stripeCustomerId)
.single();
if (error) {
console.error('Error finding user by Stripe customer ID:', error);
return null;
}
return data?.user_id || null;
}getCreditBalance function · typescript · L48-L99 (52 LOC)lib/credits.ts
export async function getCreditBalance(userId: string): Promise<CreditBalance> {
const supabase = createServerClient();
if (!supabase) {
console.error('Supabase not configured');
return { credits: 0, totalPurchased: 0, lastFreeReset: '' };
}
// Try to get existing credit record
const { data, error } = await supabase
.from('user_credits')
.select('*')
.eq('user_id', userId)
.single();
if (error && error.code !== 'PGRST116') {
// PGRST116 = not found
console.error('Error fetching credit balance:', error);
return { credits: 0, totalPurchased: 0, lastFreeReset: '' };
}
// If no record exists, create one with free credits
if (!data) {
const now = new Date().toISOString();
const { data: newData, error: insertError } = await supabase
.from('user_credits')
.insert({
user_id: userId,
credits: FREE_CREDITS_AMOUNT,
total_purchased: 0,
last_free_reset: now,
})
.select()
.saddCredits function · typescript · L155-L217 (63 LOC)lib/credits.ts
export async function addCredits(
userId: string,
bundle: CreditBundle,
stripePaymentId: string,
stripeSessionId?: string
): Promise<boolean> {
const supabase = createServerClient();
if (!supabase) {
return false;
}
const bundleConfig = CREDIT_BUNDLES[bundle];
if (!bundleConfig) {
console.error('Invalid bundle type:', bundle);
return false;
}
// Check for duplicate payment (idempotency)
const { data: existing } = await supabase
.from('credit_purchases')
.select('id')
.eq('stripe_payment_id', stripePaymentId)
.single();
if (existing) {
console.warn('Duplicate payment detected:', stripePaymentId);
return true; // Already processed, return success
}
// Get current balance
const balance = await getCreditBalance(userId);
// Add credits atomically
const { error: updateError } = await supabase
.from('user_credits')
.update({
credits: balance.credits + bundleConfig.credits,
total_purchased: balanceWant this analysis on your repo? https://repobility.com/scan/
getPurchaseHistory function · typescript · L222-L250 (29 LOC)lib/credits.ts
export async function getPurchaseHistory(
userId: string,
limit: number = 10
): Promise<CreditPurchase[]> {
const supabase = createServerClient();
if (!supabase) {
return [];
}
const { data, error } = await supabase
.from('credit_purchases')
.select('id, bundle_type, credits_purchased, amount_cents, created_at')
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(limit);
if (error) {
console.error('Error fetching purchase history:', error);
return [];
}
return (data || []).map((row) => ({
id: row.id,
bundleType: row.bundle_type as CreditBundle,
creditsPurchased: row.credits_purchased,
amountCents: row.amount_cents,
createdAt: row.created_at,
}));
}checkAndResetFreeCredits function · typescript · L256-L299 (44 LOC)lib/credits.ts
export async function checkAndResetFreeCredits(userId: string): Promise<void> {
const supabase = createServerClient();
if (!supabase) {
return;
}
const balance = await getCreditBalance(userId);
if (!balance.lastFreeReset) {
return;
}
const lastReset = new Date(balance.lastFreeReset);
const now = new Date();
// Check if a month has passed since last reset
const monthsSinceReset =
(now.getFullYear() - lastReset.getFullYear()) * 12 +
(now.getMonth() - lastReset.getMonth());
if (monthsSinceReset < 1) {
return; // Not yet eligible for reset
}
// Reset free credits (add them, don't replace existing)
const { error } = await supabase
.from('user_credits')
.update({
credits: balance.credits + FREE_CREDITS_AMOUNT,
last_free_reset: now.toISOString(),
})
.eq('user_id', userId);
if (error) {
console.error('Error resetting free credits:', error);
return;
}
// Log the reset
await supabase.from('credit_ugetAnthropic function · typescript · L5-L12 (8 LOC)lib/cv-analyzer.ts
function getAnthropic(): Anthropic {
if (!anthropicClient) {
anthropicClient = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
}
return anthropicClient;
}analyzeCV function · typescript · L126-L212 (87 LOC)lib/cv-analyzer.ts
export async function analyzeCV(cvText: string): Promise<CVAnalysisResult> {
// Handle empty or minimal CV input
const trimmedCV = cvText.trim();
if (!trimmedCV || trimmedCV.length < 50) {
return {
questions: NO_CV_QUESTIONS,
cvQuality: 'none',
};
}
try {
const prompt = CV_ANALYSIS_PROMPT.replace('{cvText}', trimmedCV.slice(0, 10000));
const response = await getAnthropic().messages.create({
model: 'claude-3-5-haiku-20241022',
max_tokens: 1000,
messages: [
{
role: 'user',
content: prompt,
},
],
});
const content = response.content[0];
if (content.type !== 'text') {
throw new Error('Unexpected response type from Claude');
}
// Parse the JSON response
let result: CVAnalysisResult;
try {
// Clean up potential markdown code blocks
let jsonText = content.text.trim();
const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)```/);
itriggerCVGeneration function · typescript · L13-L18 (6 LOC)lib/cv-auto-generate.ts
export function triggerCVGeneration(applicationId: string): void {
// Fire and forget - don't await
generateCVForApplication(applicationId).catch((error) => {
console.error(`[CV Auto-Gen] Failed for ${applicationId}:`, error);
});
}generateCVForApplication function · typescript · L20-L137 (118 LOC)lib/cv-auto-generate.ts
async function generateCVForApplication(applicationId: string): Promise<void> {
console.log(`[CV Auto-Gen] Starting for application ${applicationId}`);
const supabase = createServerClient();
if (!supabase) {
console.error('[CV Auto-Gen] Database not configured');
return;
}
// Check if CV already exists
const { data: app, error: fetchError } = await supabase
.from('applications')
.select('id, company, role, job_url, tailored_cv_url')
.eq('id', applicationId)
.single();
if (fetchError || !app) {
console.error('[CV Auto-Gen] Application not found:', fetchError);
return;
}
// Skip if already has CV or no job URL
if (app.tailored_cv_url) {
console.log('[CV Auto-Gen] CV already exists, skipping');
return;
}
if (!app.job_url) {
console.log('[CV Auto-Gen] No job URL, skipping');
return;
}
// Get master CV
const { data: cvData, error: cvError } = await supabase
.from('cvs')
.select('raw_text')
.ordergetAnthropic function · typescript · L5-L12 (8 LOC)lib/cv-enhancer.ts
function getAnthropic(): Anthropic {
if (!anthropicClient) {
anthropicClient = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
}
return anthropicClient;
}enhanceCV function · typescript · L90-L158 (69 LOC)lib/cv-enhancer.ts
export async function enhanceCV(input: CVEnhanceInput): Promise<CVEnhanceResult> {
const { cvText, answers } = input;
// Format answers for the prompt
const formattedAnswers = Object.entries(answers)
.filter(([, value]) => value && value.trim().length > 0)
.map(([key, value]) => `${formatQuestionKey(key)}: ${value}`)
.join('\n');
if (!formattedAnswers) {
// No additional info provided, just clean up the CV
if (!cvText || cvText.trim().length < 50) {
throw new Error('No CV text or answers provided');
}
return {
masterCV: cvText.trim(),
tokensUsed: 0,
};
}
// Determine which prompt to use
const isFromScratch = !cvText || cvText.trim().length < 100;
let prompt: string;
if (isFromScratch) {
prompt = FROM_SCRATCH_PROMPT.replace('{answers}', formattedAnswers);
} else {
prompt = CV_ENHANCEMENT_PROMPT
.replace('{cvText}', cvText.trim().slice(0, 8000))
.replace('{answers}', formattedAnswers);
}
trRepobility · code-quality intelligence · https://repobility.com
formatQuestionKey function · typescript · L163-L178 (16 LOC)lib/cv-enhancer.ts
function formatQuestionKey(key: string): string {
const keyMap: Record<string, string> = {
job_title: 'Current/Recent Job Title',
company_duration: 'Company and Duration',
responsibilities: 'Main Responsibilities',
achievement: 'Key Achievement',
achievements: 'Key Achievements',
skills: 'Top Skills',
education: 'Education',
target_role: 'Target Role',
recent: 'Recent Activity',
project: 'Notable Project',
};
return keyMap[key] || key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}getAnthropic function · typescript · L5-L12 (8 LOC)lib/cv-tailor.ts
function getAnthropic(): Anthropic {
if (!anthropicClient) {
anthropicClient = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
}
return anthropicClient;
}generateTailoredCV function · typescript · L285-L360 (76 LOC)lib/cv-tailor.ts
export async function generateTailoredCV(input: TailoredCVInput): Promise<TailoredCVResult> {
const { masterCV, jobDescription, company, role, feedback } = input;
if (!masterCV || masterCV.trim().length < 50) {
throw new Error('Master CV is too short or empty');
}
if (!jobDescription || jobDescription.trim().length < 50) {
throw new Error('Job description is too short or empty');
}
// Build the prompt with all context
const feedbackSection = feedback
? `USER FEEDBACK FOR REVISION:\n${feedback}\n\nPlease incorporate this feedback while generating the CV.`
: '';
const prompt = CV_GENERATION_PROMPT
.replace('{masterCV}', masterCV.trim())
.replace('{jobDescription}', jobDescription.trim().slice(0, 8000))
.replace('{company}', company || 'Unknown Company')
.replace('{role}', role || 'Unknown Role')
.replace('{feedback}', feedbackSection);
try {
const response = await getAnthropic().messages.create({
model: 'claude-sonnet-4