Files
davila7--claude-code-templates/dashboard/src/components/JobsView.tsx
T
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

751 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useMemo } from 'react';
interface Job {
id: string;
company: string;
position: string;
location: string;
remote: boolean;
salary: string;
description: string;
applyUrl: string;
source: string;
sourceUrl: string;
postedAt: string;
tags: string[];
companyIcon: string;
}
interface JobsData {
lastUpdated: string;
totalJobs: number;
sources: string[];
jobs: Job[];
}
type LocationFilter = 'all' | 'remote' | 'onsite';
type SortOption = 'recent' | 'company' | 'salary';
const SOURCE_COLORS: Record<string, string> = {
HackerNews: 'bg-orange-500/15 text-orange-400 border-orange-500/20',
RemoteOK: 'bg-blue-500/15 text-blue-400 border-blue-500/20',
WeWorkRemotely: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/20',
Anthropic: 'bg-purple-500/15 text-purple-400 border-purple-500/20',
};
function useGlobalAuth() {
const [state, setState] = useState<{ isSignedIn: boolean; isLoaded: boolean }>({
isSignedIn: false, isLoaded: false,
});
useEffect(() => {
function check() {
const clerk = (window as any).Clerk;
if (clerk?.loaded) {
const signedIn = !!clerk.user;
setState((prev) => {
if (prev.isLoaded && prev.isSignedIn === signedIn) return prev;
return { isSignedIn: signedIn, isLoaded: true };
});
}
}
check();
const interval = setInterval(check, 500);
const handleChange = () => check();
window.addEventListener('clerk:session', handleChange);
return () => { clearInterval(interval); window.removeEventListener('clerk:session', handleChange); };
}, []);
return state;
}
function timeAgo(dateStr: string): string {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return '';
const now = new Date();
const days = Math.floor((now.getTime() - date.getTime()) / 86400000);
if (days < 0 || isNaN(days)) return '';
if (days === 0) return 'Today';
if (days === 1) return '1d ago';
if (days < 30) return `${days}d ago`;
if (days < 60) return '1mo ago';
return `${Math.floor(days / 30)}mo ago`;
} catch {
return '';
}
}
function safeUrl(url: string): string {
return /^https?:\/\//i.test(url) ? url : '#';
}
export default function JobsView() {
const [data, setData] = useState<JobsData | null>(null);
const [loading, setLoading] = useState(true);
const [locationFilter, setLocationFilter] = useState<LocationFilter>('all');
const [sourceFilter, setSourceFilter] = useState<string>('all');
const [companyFilter, setCompanyFilter] = useState<string>('all');
const [selectedTag, setSelectedTag] = useState<string>('all');
const [sortBy, setSortBy] = useState<SortOption>('recent');
const [searchQuery, setSearchQuery] = useState('');
const { isSignedIn, isLoaded } = useGlobalAuth();
useEffect(() => {
fetch('/claude-jobs.json')
.then((r) => r.json())
.then((d) => { setData(d); setLoading(false); })
.catch(() => setLoading(false));
}, []);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (!target.closest('.company-dropdown')) {
setIsCompanyDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const filteredJobs = useMemo(() => {
if (!data) return [];
let filtered = data.jobs.filter((job) => {
if (locationFilter === 'remote' && !job.remote) return false;
if (locationFilter === 'onsite' && job.remote) return false;
if (sourceFilter !== 'all' && job.source !== sourceFilter) return false;
if (companyFilter !== 'all' && job.company !== companyFilter) return false;
if (selectedTag !== 'all' && !job.tags.includes(selectedTag)) return false;
if (searchQuery) {
const q = searchQuery.toLowerCase();
const text = `${job.company} ${job.position} ${job.location} ${job.tags.join(' ')}`.toLowerCase();
if (!text.includes(q)) return false;
}
return true;
});
// Sort jobs
filtered.sort((a, b) => {
if (sortBy === 'recent') {
return new Date(b.postedAt || 0).getTime() - new Date(a.postedAt || 0).getTime();
} else if (sortBy === 'company') {
return a.company.localeCompare(b.company);
} else if (sortBy === 'salary') {
// Sort by salary (jobs with salary first)
if (a.salary && !b.salary) return -1;
if (!a.salary && b.salary) return 1;
return 0;
}
return 0;
});
return filtered;
}, [data, locationFilter, sourceFilter, companyFilter, selectedTag, sortBy, searchQuery]);
// Get unique companies and tags for filters
const companies = useMemo(() => {
if (!data) return [];
const uniqueCompanies = Array.from(new Set(data.jobs.map(j => j.company))).sort();
return uniqueCompanies;
}, [data]);
// Get company logo for a given company name
const getCompanyLogo = (companyName: string): string => {
if (!data) return '';
const job = data.jobs.find(j => j.company === companyName);
// If companyIcon exists in data, use it
if (job?.companyIcon) return job.companyIcon;
// Otherwise, try to fetch from Clearbit Logo API
// Format: https://logo.clearbit.com/{domain}
const domain = companyName.toLowerCase()
.replace(/\s+/g, '')
.replace(/[^a-z0-9]/g, '');
return `https://logo.clearbit.com/${domain}.com`;
};
const allTags = useMemo(() => {
if (!data) return [];
const tagSet = new Set<string>();
data.jobs.forEach(job => job.tags.forEach(tag => tagSet.add(tag)));
return Array.from(tagSet).sort();
}, [data]);
// Custom dropdown state
const [isCompanyDropdownOpen, setIsCompanyDropdownOpen] = useState(false);
function handleJobClick(e: React.MouseEvent, job: Job) {
if (!isLoaded) {
e.preventDefault();
return;
}
if (!isSignedIn) {
e.preventDefault();
(window as any).Clerk?.openSignIn?.();
}
// If signed in, let the <a> navigate normally
}
if (loading) {
return (
<div className="px-6 py-20 flex flex-col items-center gap-3">
<div className="w-5 h-5 border-2 border-[var(--color-text-tertiary)] border-t-transparent rounded-full animate-spin" />
<span className="text-[13px] text-[var(--color-text-tertiary)]">Loading jobs...</span>
</div>
);
}
if (!data || data.jobs.length === 0) {
return (
<div className="flex flex-col items-center justify-center px-6 py-24">
{/* Icon */}
<div className="w-20 h-20 rounded-2xl bg-[var(--color-surface-2)] border border-[var(--color-border)] flex items-center justify-center mb-6">
<svg className="w-10 h-10 text-[var(--color-text-tertiary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
<rect x="2" y="7" width="20" height="14" rx="2" ry="2" />
<path d="M16 7V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v2" />
<line x1="12" y1="12" x2="12" y2="12.01" />
</svg>
</div>
{/* Message */}
<h3 className="text-[15px] font-semibold text-[var(--color-text-primary)] mb-2">No jobs available yet</h3>
<p className="text-[13px] text-[var(--color-text-secondary)] text-center max-w-md leading-relaxed">
We're currently curating exciting opportunities for AI developers. Check back soon for new job postings!
</p>
</div>
);
}
const remoteCount = data.jobs.filter((j) => j.remote).length;
const onsiteCount = data.jobs.length - remoteCount;
const showAuthGate = isLoaded && !isSignedIn;
return (
<div>
{/* Header */}
<div className="px-6 pt-5 pb-2">
<h1 className="text-lg font-semibold text-[var(--color-text-primary)] flex items-center gap-2">
Jobs Requiring Claude Code
<picture>
<source srcSet="/claude-code-logo.webp" type="image/webp" />
<img src="/claude-code-logo.png" alt="Claude Code" className="w-6 h-6 inline-block" loading="lazy" />
</picture>
</h1>
<p className="text-[13px] text-[var(--color-text-tertiary)] mt-1">
Companies actively hiring developers who use Claude Code in their workflow.
Updated daily from HackerNews "Who is Hiring", RemoteOK, and more.
</p>
</div>
{/* Auth banner for non-signed-in users */}
{showAuthGate && (
<div className="mx-6 mt-2 mb-1 bg-gradient-to-r from-purple-500/10 via-blue-500/10 to-emerald-500/10 border border-[var(--color-border)] rounded-lg px-4 py-3 flex items-center gap-3">
<svg className="w-5 h-5 text-purple-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0110 0v4" />
</svg>
<div className="flex-1">
<p className="text-[13px] text-[var(--color-text-primary)]">
<span className="font-medium">Sign in</span>
<span className="text-[var(--color-text-secondary)]"> to view job details and apply to positions</span>
</p>
</div>
<button
onClick={() => (window as any).Clerk?.openSignIn?.()}
className="px-3 py-1.5 bg-[var(--color-surface-3)] hover:bg-[var(--color-surface-4)] text-[12px] font-medium text-[var(--color-text-primary)] rounded-md transition-colors"
>
Sign In
</button>
</div>
)}
{/* Stats cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 px-6 py-4">
{[
{ label: 'Total Jobs', value: String(data.totalJobs) },
{ label: 'Remote', value: String(remoteCount) },
{ label: 'On-site', value: String(onsiteCount) },
{ label: 'Sources', value: String(data.sources.length) },
].map((s) => (
<div key={s.label} className="bg-[var(--color-card-bg)] border border-[var(--color-border)] rounded-lg px-4 py-3">
<div className="text-[18px] font-semibold text-[var(--color-text-primary)] tabular-nums">{s.value}</div>
<div className="text-[11px] text-[var(--color-text-tertiary)] mt-0.5">{s.label}</div>
</div>
))}
</div>
{/* Divider */}
<div className="border-t border-[var(--color-border)]" />
{/* Filter bar - Enhanced */}
<div className="px-6 py-4 space-y-3">
{/* Top row: Location, Source, Company, Sort */}
<div className="flex flex-wrap items-center gap-2">
{/* Location filter */}
<div className="flex items-center gap-1 bg-[var(--color-surface-2)] rounded-lg p-0.5">
{(['all', 'remote', 'onsite'] as LocationFilter[]).map((loc) => (
<button
key={loc}
onClick={() => setLocationFilter(loc)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[12px] font-medium transition-all ${
locationFilter === loc
? 'bg-[var(--color-surface-3)] text-[var(--color-text-primary)] shadow-sm'
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]'
}`}
>
{loc === 'all' ? (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
All Locations
</>
) : loc === 'remote' ? (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<path strokeLinecap="round" strokeLinejoin="round" d="M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" />
</svg>
Remote
</>
) : (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
On-site
</>
)}
</button>
))}
</div>
{/* Source filter dropdown */}
<div className="relative">
<select
value={sourceFilter}
onChange={(e) => setSourceFilter(e.target.value)}
className="appearance-none bg-[var(--color-surface-2)] border border-[var(--color-border)] rounded-lg text-[12px] font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-3 py-1.5 pr-8 outline-none cursor-pointer transition-colors"
>
<option value="all">All Sources</option>
{data.sources.map((src) => (
<option key={src} value={src}>{src}</option>
))}
</select>
<svg className="absolute right-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[var(--color-text-tertiary)] pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</div>
{/* Company filter dropdown - Custom with logos */}
<div className="relative company-dropdown">
<button
onClick={() => setIsCompanyDropdownOpen(!isCompanyDropdownOpen)}
className="flex items-center gap-2 bg-[var(--color-surface-2)] border border-[var(--color-border)] rounded-lg text-[12px] font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] pl-3 pr-8 py-1.5 outline-none cursor-pointer transition-colors min-w-[180px]"
>
{companyFilter === 'all' ? (
<>
<svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<span>All Companies ({companies.length})</span>
</>
) : (
<>
<div className="w-4 h-4 rounded bg-[var(--color-surface-3)] border border-[var(--color-border)] flex items-center justify-center shrink-0 overflow-hidden">
<img
src={getCompanyLogo(companyFilter)}
alt=""
className="w-3 h-3 object-contain"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
const fallback = target.nextElementSibling as HTMLElement;
if (fallback) fallback.style.display = 'flex';
}}
/>
<span className="text-[8px] font-bold text-[var(--color-text-tertiary)]" style={{ display: 'none' }}>
{companyFilter.charAt(0).toUpperCase()}
</span>
</div>
<span className="truncate">{companyFilter}</span>
</>
)}
<svg className="absolute right-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[var(--color-text-tertiary)] pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Dropdown menu */}
{isCompanyDropdownOpen && (
<div className="absolute top-full left-0 mt-1 w-72 max-h-80 overflow-y-auto bg-[var(--color-surface-2)] border border-[var(--color-border)] rounded-lg shadow-lg z-50">
<button
onClick={() => {
setCompanyFilter('all');
setIsCompanyDropdownOpen(false);
}}
className={`w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-left transition-colors ${
companyFilter === 'all'
? 'bg-[var(--color-surface-3)] text-[var(--color-text-primary)] font-medium'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-primary)]'
}`}
>
<svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<span>All Companies ({companies.length})</span>
</button>
<div className="border-t border-[var(--color-border)] my-1" />
{companies.map((company) => (
<button
key={company}
onClick={() => {
setCompanyFilter(company);
setIsCompanyDropdownOpen(false);
}}
className={`w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-left transition-colors ${
companyFilter === company
? 'bg-[var(--color-surface-3)] text-[var(--color-text-primary)] font-medium'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-primary)]'
}`}
>
<div className="w-5 h-5 rounded bg-[var(--color-surface-3)] border border-[var(--color-border)] flex items-center justify-center shrink-0 overflow-hidden">
<img
src={getCompanyLogo(company)}
alt=""
className="w-4 h-4 object-contain"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
const fallback = target.nextElementSibling as HTMLElement;
if (fallback) fallback.style.display = 'flex';
}}
/>
<span className="text-[9px] font-bold text-[var(--color-text-tertiary)]" style={{ display: 'none' }}>
{company.charAt(0).toUpperCase()}
</span>
</div>
<span className="truncate">{company}</span>
</button>
))}
</div>
)}
</div>
{/* Sort dropdown */}
<div className="relative">
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as SortOption)}
className="appearance-none bg-[var(--color-surface-2)] border border-[var(--color-border)] rounded-lg text-[12px] font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] pl-9 pr-8 py-1.5 outline-none cursor-pointer transition-colors"
>
<option value="recent">Most Recent</option>
<option value="company">Company A-Z</option>
<option value="salary">With Salary</option>
</select>
<svg className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[var(--color-text-tertiary)] pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
{sortBy === 'recent' ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
) : sortBy === 'company' ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
)}
</svg>
<svg className="absolute right-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[var(--color-text-tertiary)] pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</div>
{/* Search */}
<div className="ml-auto relative">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[var(--color-text-tertiary)] pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8" />
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-4.35-4.35" />
</svg>
<input
type="text"
placeholder="Search jobs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-[var(--color-surface-2)] border border-[var(--color-border)] rounded-lg text-[12px] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] pl-9 pr-3 py-1.5 w-56 outline-none focus:border-[var(--color-border-hover)] transition-colors"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
>
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
</div>
{/* Bottom row: Tags filter */}
{allTags.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-[11px] font-medium text-[var(--color-text-tertiary)] uppercase tracking-wide">Tags:</span>
<div className="flex items-center gap-1.5 flex-wrap">
<button
onClick={() => setSelectedTag('all')}
className={`px-2.5 py-1 rounded-md text-[11px] font-medium transition-all ${
selectedTag === 'all'
? 'bg-[var(--color-surface-3)] text-[var(--color-text-primary)] border border-[var(--color-border)]'
: 'bg-[var(--color-surface-2)] text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-3)]'
}`}
>
All
</button>
{allTags.slice(0, 12).map((tag) => (
<button
key={tag}
onClick={() => setSelectedTag(tag)}
className={`px-2.5 py-1 rounded-md text-[11px] font-medium transition-all ${
selectedTag === tag
? 'bg-blue-500/15 text-blue-400 border border-blue-500/30'
: 'bg-[var(--color-surface-2)] text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-3)]'
}`}
>
{tag}
</button>
))}
{allTags.length > 12 && (
<span className="text-[11px] text-[var(--color-text-tertiary)] px-2">
+{allTags.length - 12} more
</span>
)}
</div>
</div>
)}
{/* Active filters indicator */}
{(locationFilter !== 'all' || sourceFilter !== 'all' || companyFilter !== 'all' || selectedTag !== 'all' || searchQuery) && (
<div className="flex items-center gap-2 pt-1">
<span className="text-[11px] text-[var(--color-text-tertiary)]">Active filters:</span>
<div className="flex items-center gap-1.5 flex-wrap">
{locationFilter !== 'all' && (
<span className="inline-flex items-center gap-1 text-[11px] bg-blue-500/15 text-blue-400 px-2 py-0.5 rounded-full">
{locationFilter}
<button onClick={() => setLocationFilter('all')} className="hover:text-blue-300">×</button>
</span>
)}
{sourceFilter !== 'all' && (
<span className="inline-flex items-center gap-1 text-[11px] bg-orange-500/15 text-orange-400 px-2 py-0.5 rounded-full">
{sourceFilter}
<button onClick={() => setSourceFilter('all')} className="hover:text-orange-300">×</button>
</span>
)}
{companyFilter !== 'all' && (
<span className="inline-flex items-center gap-1 text-[11px] bg-purple-500/15 text-purple-400 px-2 py-0.5 rounded-full">
{companyFilter}
<button onClick={() => setCompanyFilter('all')} className="hover:text-purple-300">×</button>
</span>
)}
{selectedTag !== 'all' && (
<span className="inline-flex items-center gap-1 text-[11px] bg-emerald-500/15 text-emerald-400 px-2 py-0.5 rounded-full">
{selectedTag}
<button onClick={() => setSelectedTag('all')} className="hover:text-emerald-300">×</button>
</span>
)}
{searchQuery && (
<span className="inline-flex items-center gap-1 text-[11px] bg-pink-500/15 text-pink-400 px-2 py-0.5 rounded-full">
"{searchQuery}"
<button onClick={() => setSearchQuery('')} className="hover:text-pink-300">×</button>
</span>
)}
<button
onClick={() => {
setLocationFilter('all');
setSourceFilter('all');
setCompanyFilter('all');
setSelectedTag('all');
setSearchQuery('');
}}
className="text-[11px] text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] underline"
>
Clear all
</button>
</div>
</div>
)}
</div>
{/* Results count */}
<div className="px-6 pb-3 flex items-center justify-between">
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">
Showing {filteredJobs.length} of {data.totalJobs} job{filteredJobs.length !== 1 ? 's' : ''}
</span>
{filteredJobs.length === 0 && (
<button
onClick={() => {
setLocationFilter('all');
setSourceFilter('all');
setCompanyFilter('all');
setSelectedTag('all');
setSearchQuery('');
}}
className="text-[12px] text-blue-400 hover:text-blue-300 underline"
>
Reset filters
</button>
)}
</div>
{/* Job cards */}
{filteredJobs.length === 0 ? (
<div className="px-6 pb-8 flex flex-col items-center justify-center py-12">
<div className="w-16 h-16 rounded-full bg-[var(--color-surface-2)] border border-[var(--color-border)] flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-[var(--color-text-tertiary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<h3 className="text-[14px] font-semibold text-[var(--color-text-primary)] mb-1">No jobs found</h3>
<p className="text-[12px] text-[var(--color-text-tertiary)] text-center max-w-sm">
Try adjusting your filters or search query to find more opportunities.
</p>
</div>
) : (
<div className="px-6 pb-8 grid grid-cols-1 md:grid-cols-2 gap-3">
{filteredJobs.map((job) => (
<a
key={job.id}
href={isSignedIn ? safeUrl(job.applyUrl) : '#'}
target={isSignedIn ? '_blank' : undefined}
rel={isSignedIn ? 'noopener noreferrer' : undefined}
onClick={(e) => handleJobClick(e, job)}
className="block bg-[var(--color-card-bg)] border border-[var(--color-border)] rounded-lg p-4 hover:border-[var(--color-border-hover)] hover:bg-[var(--color-card-hover)] hover:shadow-lg transition-all group relative"
>
<div className="flex items-start gap-3">
{/* Company icon - Enhanced with internet fallback */}
<div className="w-12 h-12 rounded-lg bg-[var(--color-surface-2)] border border-[var(--color-border)] flex items-center justify-center shrink-0 overflow-hidden group-hover:border-[var(--color-border-hover)] transition-colors">
<img
src={job.companyIcon || getCompanyLogo(job.company)}
alt={job.company}
className="w-7 h-7 object-contain"
style={{ display: 'block' }}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
const fallback = target.nextElementSibling as HTMLElement;
if (fallback) fallback.style.display = 'flex';
}}
/>
<span
className="text-[16px] font-bold text-[var(--color-text-tertiary)]"
style={{ display: 'none' }}
>
{job.company.charAt(0).toUpperCase()}
</span>
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start gap-2 flex-wrap">
<h3 className="text-[14px] font-semibold text-[var(--color-text-primary)] group-hover:text-[var(--color-text-primary)] transition-colors flex-1">
{job.position}
</h3>
{job.salary && (
<span className="flex items-center gap-1 text-[11px] font-semibold bg-emerald-500/15 text-emerald-400 px-2 py-1 rounded border border-emerald-500/20">
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{job.salary}
</span>
)}
</div>
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
<span className="text-[13px] font-medium text-[var(--color-text-secondary)]">{job.company}</span>
<span className="text-[10px] text-[var(--color-text-tertiary)]">•</span>
<span className="text-[12px] text-[var(--color-text-tertiary)]">{job.location}</span>
</div>
<div className="flex items-center gap-1.5 mt-2 flex-wrap">
{job.remote && (
<span className="flex items-center gap-1 text-[10px] font-semibold bg-blue-500/15 text-blue-400 px-2 py-1 rounded border border-blue-500/20">
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<path strokeLinecap="round" strokeLinejoin="round" d="M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" />
</svg>
Remote
</span>
)}
<span className={`text-[10px] font-semibold px-2 py-1 rounded border ${SOURCE_COLORS[job.source] || 'bg-[var(--color-surface-3)] text-[var(--color-text-secondary)]'}`}>
{job.source}
</span>
{job.postedAt && (
<span className="flex items-center gap-1 text-[11px] text-[var(--color-text-tertiary)] ml-auto">
<svg className="w-3 h-3" 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>
{timeAgo(job.postedAt)}
</span>
)}
</div>
{job.description && (
<p className="text-[12px] text-[var(--color-text-secondary)] mt-2.5 line-clamp-2 leading-relaxed">{job.description}</p>
)}
{job.tags.length > 0 && (
<div className="flex items-center gap-1.5 mt-2.5 flex-wrap">
{job.tags.slice(0, 6).map((tag) => (
<span
key={tag}
className="text-[10px] text-[var(--color-text-secondary)] bg-[var(--color-surface-2)] px-2 py-0.5 rounded border border-[var(--color-border)] hover:border-[var(--color-border-hover)] transition-colors cursor-pointer"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setSelectedTag(tag);
}}
>
{tag}
</span>
))}
{job.tags.length > 6 && (
<span className="text-[10px] text-[var(--color-text-tertiary)]">
+{job.tags.length - 6}
</span>
)}
</div>
)}
</div>
{/* Right side: apply arrow or lock icon */}
{showAuthGate ? (
<svg
className="w-4 h-4 text-[var(--color-text-tertiary)] shrink-0 mt-1"
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}
>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0110 0v4" />
</svg>
) : (
<svg
className="w-4 h-4 text-[var(--color-text-tertiary)] group-hover:text-[var(--color-text-secondary)] group-hover:translate-x-0.5 group-hover:-translate-y-0.5 shrink-0 mt-1 transition-all"
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
)}
</div>
</a>
))}
</div>
)}
{/* Footer */}
<div className="px-6 pb-6 text-center">
<p className="text-[11px] text-[var(--color-text-tertiary)]">
Last updated: {new Date(data.lastUpdated).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
{' | '}Data sourced from HackerNews "Who is Hiring", RemoteOK, WeWorkRemotely, and Anthropic Careers
</p>
</div>
</div>
);
}