395 lines
14 KiB
TypeScript
395 lines
14 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
JobApplication,
|
|
JobFeedItem,
|
|
UserProfile,
|
|
NotificationLog,
|
|
CalendarInterviewEvent,
|
|
ApplicationStatus
|
|
} from './types';
|
|
import {
|
|
INITIAL_USER_PROFILE,
|
|
INITIAL_APPLICATIONS,
|
|
INITIAL_JOB_FEEDS,
|
|
INITIAL_CALENDAR_EVENTS,
|
|
INITIAL_NOTIFICATIONS
|
|
} from './data/mockData';
|
|
import {
|
|
fetchApplications,
|
|
saveApplication,
|
|
updateApplicationStatus,
|
|
deleteApplicationRecord,
|
|
fetchUserProfile,
|
|
saveUserProfile,
|
|
fetchNotifications,
|
|
saveNotificationLog,
|
|
fetchCalendarEvents,
|
|
saveCalendarEvent
|
|
} from './lib/localStorageEngine';
|
|
|
|
import { Navbar } from './components/Navbar';
|
|
import { Sidebar } from './components/Sidebar';
|
|
import { ApplicationsTracker } from './components/ApplicationsTracker';
|
|
import { WalkthroughTailor } from './components/WalkthroughTailor';
|
|
import { AutonomousEngine } from './components/AutonomousEngine';
|
|
import { JobFeeds } from './components/JobFeeds';
|
|
import { AnalyticsDashboard } from './components/AnalyticsDashboard';
|
|
import { CalendarScheduler } from './components/CalendarScheduler';
|
|
import { NotificationCenter } from './components/NotificationCenter';
|
|
import { ProfileSettings } from './components/ProfileSettings';
|
|
import { GoogleWorkspaceHub } from './components/GoogleWorkspaceHub';
|
|
import { TailoredResumeModal } from './components/TailoredResumeModal';
|
|
import { NewApplicationModal } from './components/NewApplicationModal';
|
|
|
|
export default function App() {
|
|
const [activeTab, setActiveTab] = useState<string>('applications');
|
|
|
|
// App state
|
|
const [applications, setApplications] = useState<JobApplication[]>(INITIAL_APPLICATIONS);
|
|
const [userProfile, setUserProfile] = useState<UserProfile>(INITIAL_USER_PROFILE);
|
|
const [jobFeeds, setJobFeeds] = useState<JobFeedItem[]>(INITIAL_JOB_FEEDS);
|
|
const [calendarEvents, setCalendarEvents] = useState<CalendarInterviewEvent[]>(INITIAL_CALENDAR_EVENTS);
|
|
const [notifications, setNotifications] = useState<NotificationLog[]>(INITIAL_NOTIFICATIONS);
|
|
|
|
// Modals & UI States
|
|
const [isAutoApplying, setIsAutoApplying] = useState(false);
|
|
const [selectedAppForModal, setSelectedAppForModal] = useState<JobApplication | null>(null);
|
|
const [isNewAppModalOpen, setIsNewAppModalOpen] = useState(false);
|
|
const [walkthroughInitialUrl, setWalkthroughInitialUrl] = useState('');
|
|
|
|
// Initial Load from Firestore & Live Job Feeds
|
|
useEffect(() => {
|
|
async function loadData() {
|
|
try {
|
|
const firestoreApps = await fetchApplications();
|
|
if (firestoreApps.length > 0) setApplications(firestoreApps);
|
|
|
|
const profile = await fetchUserProfile();
|
|
if (profile) setUserProfile(profile);
|
|
|
|
const events = await fetchCalendarEvents();
|
|
if (events.length > 0) setCalendarEvents(events);
|
|
|
|
const notifs = await fetchNotifications();
|
|
if (notifs.length > 0) setNotifications(notifs);
|
|
|
|
// Fetch live job feeds from APIs + RSS
|
|
const feedsRes = await fetch('/api/jobs/fetch-feeds');
|
|
if (feedsRes.ok) {
|
|
const feedsData = await feedsRes.json();
|
|
if (feedsData.success && Array.isArray(feedsData.jobs) && feedsData.jobs.length > 0) {
|
|
setJobFeeds(prev => [...feedsData.jobs, ...prev]);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn('Initial load fallback to initial dataset:', err);
|
|
}
|
|
}
|
|
loadData();
|
|
}, []);
|
|
|
|
// Handlers
|
|
const handleUpdateStatus = async (id: string, newStatus: ApplicationStatus, note?: string) => {
|
|
setApplications(prev => prev.map(a => a.id === id ? { ...a, status: newStatus } : a));
|
|
await updateApplicationStatus(id, newStatus, note);
|
|
|
|
// Send email alert for important status updates
|
|
if (['Interviewing', 'Offer', 'Screening'].includes(newStatus)) {
|
|
const app = applications.find(a => a.id === id);
|
|
if (app) {
|
|
const notif = await saveNotificationLog({
|
|
timestamp: new Date().toISOString(),
|
|
type: 'STATUS_CHANGE',
|
|
recipient: userProfile.email,
|
|
subject: `Application Status Alert: ${app.company} — ${newStatus}`,
|
|
body: `Your job application for ${app.role} at ${app.company} has moved to ${newStatus}.`,
|
|
status: 'SENT',
|
|
platform: 'Gmail'
|
|
});
|
|
setNotifications(prev => [notif, ...prev]);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleDeleteApplication = async (id: string) => {
|
|
setApplications(prev => prev.filter(a => a.id !== id));
|
|
await deleteApplicationRecord(id);
|
|
};
|
|
|
|
const handleSaveApplicationRecord = async (appData: Omit<JobApplication, 'id'>) => {
|
|
const saved = await saveApplication(appData);
|
|
setApplications(prev => [saved, ...prev.filter(a => a.id !== saved.id)]);
|
|
|
|
// Notification alert
|
|
const notif = await saveNotificationLog({
|
|
timestamp: new Date().toISOString(),
|
|
type: 'AUTO_APPLY',
|
|
recipient: userProfile.email,
|
|
subject: `Application Logged: ${saved.company} (${saved.role})`,
|
|
body: `Successfully logged application for ${saved.role} at ${saved.company} via ${saved.platform}. Match score: ${saved.matchScore}%.`,
|
|
status: 'SENT',
|
|
platform: 'Gmail'
|
|
});
|
|
setNotifications(prev => [notif, ...prev]);
|
|
};
|
|
|
|
const handleSaveUserProfile = async (updated: UserProfile) => {
|
|
setUserProfile(updated);
|
|
await saveUserProfile(updated);
|
|
};
|
|
|
|
const handleToggleAutoApply = () => {
|
|
const updated = {
|
|
...userProfile,
|
|
autoApplyCriteria: {
|
|
...userProfile.autoApplyCriteria,
|
|
enabled: !userProfile.autoApplyCriteria.enabled
|
|
}
|
|
};
|
|
handleSaveUserProfile(updated);
|
|
};
|
|
|
|
// Run Autonomous Auto-Apply Engine Batch
|
|
const handleRunAutoApplyBatch = async () => {
|
|
setIsAutoApplying(true);
|
|
try {
|
|
const res = await fetch('/api/gemini/auto-apply-run', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
jobFeeds,
|
|
userProfile,
|
|
criteria: userProfile.autoApplyCriteria
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success && Array.isArray(data.results)) {
|
|
for (const item of data.results) {
|
|
if (item.autoApplied) {
|
|
const feed = jobFeeds.find(f => f.id === item.jobId);
|
|
await handleSaveApplicationRecord({
|
|
company: item.company,
|
|
role: item.role,
|
|
platform: feed?.platform || 'Greenhouse',
|
|
location: feed?.location || 'San Francisco, CA',
|
|
salaryRange: feed?.salary || '$180,000 - $220,000',
|
|
status: 'Applied',
|
|
matchScore: item.matchScore,
|
|
appliedAt: new Date().toISOString(),
|
|
jobUrl: feed?.url,
|
|
jobDescription: feed?.description,
|
|
notes: item.reason,
|
|
autoApplied: true,
|
|
timeline: [{ date: new Date().toLocaleDateString(), action: 'Autonomous AI Auto-Applied' }]
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Autonomous auto-apply failed:', err);
|
|
} finally {
|
|
setIsAutoApplying(false);
|
|
}
|
|
};
|
|
|
|
// Schedule Interview
|
|
const handleScheduleInterview = async (eventData: Omit<CalendarInterviewEvent, 'id'>) => {
|
|
const res = await fetch('/api/calendar/schedule-interview', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ ...eventData, userEmail: userProfile.email })
|
|
});
|
|
const data = await res.json();
|
|
|
|
const savedEvent = await saveCalendarEvent({
|
|
...eventData,
|
|
googleCalendarEventId: data.googleCalendarEventId,
|
|
syncedToGoogle: true
|
|
});
|
|
|
|
setCalendarEvents(prev => [...prev.filter(e => e.id !== savedEvent.id), savedEvent]);
|
|
|
|
// Update application status to Interviewing
|
|
if (eventData.applicationId) {
|
|
await handleUpdateStatus(eventData.applicationId, 'Interviewing', `Interview scheduled for ${new Date(eventData.startTime).toLocaleDateString()}`);
|
|
}
|
|
|
|
// Log notification
|
|
const notif = await saveNotificationLog({
|
|
timestamp: new Date().toISOString(),
|
|
type: 'INTERVIEW_SCHEDULED',
|
|
recipient: userProfile.email,
|
|
subject: `Google Calendar Synced: ${eventData.company} Interview`,
|
|
body: `Interview for ${eventData.role} at ${eventData.company} scheduled for ${new Date(eventData.startTime).toLocaleString()}. Synced to Google Calendar.`,
|
|
status: 'SENT',
|
|
platform: 'Google Calendar & Gmail'
|
|
});
|
|
setNotifications(prev => [notif, ...prev]);
|
|
};
|
|
|
|
// Send test email
|
|
const handleSendTestEmail = async (recipient: string, subject: string, body: string) => {
|
|
const res = await fetch('/api/notifications/send-email', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ recipient, subject, body })
|
|
});
|
|
const data = await res.json();
|
|
|
|
const notif = await saveNotificationLog({
|
|
timestamp: new Date().toISOString(),
|
|
type: 'SYSTEM',
|
|
recipient,
|
|
subject,
|
|
body,
|
|
status: 'SENT',
|
|
platform: 'Gmail'
|
|
});
|
|
setNotifications(prev => [notif, ...prev]);
|
|
};
|
|
|
|
// Navigate & Tailor
|
|
const handleNavigateToWalkthrough = (jobUrl?: string) => {
|
|
if (jobUrl) setWalkthroughInitialUrl(jobUrl);
|
|
setActiveTab('walkthrough');
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col font-sans antialiased selection:bg-indigo-500 selection:text-white">
|
|
|
|
{/* Top Navigation */}
|
|
<Navbar
|
|
activeTab={activeTab}
|
|
autoApplyEnabled={userProfile.autoApplyCriteria.enabled}
|
|
onToggleAutoApply={handleToggleAutoApply}
|
|
onOpenNewAppModal={() => setIsNewAppModalOpen(true)}
|
|
onRunAutoApplyNow={handleRunAutoApplyBatch}
|
|
isAutoApplying={isAutoApplying}
|
|
/>
|
|
|
|
{/* Main Container */}
|
|
<div className="flex-1 flex flex-col md:flex-row max-w-7xl w-full mx-auto">
|
|
|
|
{/* Left Sidebar */}
|
|
<Sidebar
|
|
activeTab={activeTab}
|
|
setActiveTab={setActiveTab}
|
|
pendingCount={applications.filter(a => ['Applied', 'Screening'].includes(a.status)).length}
|
|
autoAppliedCount={applications.filter(a => a.autoApplied).length}
|
|
/>
|
|
|
|
{/* Main Workspace Area */}
|
|
<main className="flex-1 p-4 sm:p-6 overflow-y-auto">
|
|
{activeTab === 'applications' && (
|
|
<ApplicationsTracker
|
|
applications={applications}
|
|
onUpdateStatus={handleUpdateStatus}
|
|
onDeleteApplication={handleDeleteApplication}
|
|
onViewTailoredResume={(app) => setSelectedAppForModal(app)}
|
|
onOpenNewAppModal={() => setIsNewAppModalOpen(true)}
|
|
onNavigateToWalkthrough={handleNavigateToWalkthrough}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'walkthrough' && (
|
|
<WalkthroughTailor
|
|
userProfile={userProfile}
|
|
initialJobUrl={walkthroughInitialUrl}
|
|
onSaveTailoredApplication={handleSaveApplicationRecord}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'auto-apply' && (
|
|
<AutonomousEngine
|
|
userProfile={userProfile}
|
|
jobFeeds={jobFeeds}
|
|
autoAppliedApps={applications.filter(a => a.autoApplied)}
|
|
onSaveProfile={handleSaveUserProfile}
|
|
onRunAutoApplyBatch={handleRunAutoApplyBatch}
|
|
isAutoApplying={isAutoApplying}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'feeds' && (
|
|
<JobFeeds
|
|
jobFeeds={jobFeeds}
|
|
onTailorJobFeed={(item) => handleNavigateToWalkthrough(item.url)}
|
|
onQuickApplyFeed={async (item) => {
|
|
await handleSaveApplicationRecord({
|
|
company: item.company,
|
|
role: item.title,
|
|
platform: item.platform,
|
|
location: item.location,
|
|
salaryRange: item.salary,
|
|
status: 'Applied',
|
|
matchScore: item.estimatedMatch,
|
|
appliedAt: new Date().toISOString(),
|
|
jobUrl: item.url,
|
|
jobDescription: item.description,
|
|
autoApplied: true,
|
|
timeline: [{ date: new Date().toLocaleDateString(), action: 'Quick AI Auto-Applied from feed' }]
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'analytics' && (
|
|
<AnalyticsDashboard applications={applications} />
|
|
)}
|
|
|
|
{activeTab === 'calendar' && (
|
|
<CalendarScheduler
|
|
events={calendarEvents}
|
|
applications={applications}
|
|
onScheduleInterview={handleScheduleInterview}
|
|
userEmail={userProfile.email}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'notifications' && (
|
|
<NotificationCenter
|
|
notifications={notifications}
|
|
onSendTestEmail={handleSendTestEmail}
|
|
userEmail={userProfile.email}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'profile' && (
|
|
<ProfileSettings
|
|
userProfile={userProfile}
|
|
onSaveProfile={handleSaveUserProfile}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'workspace' && (
|
|
<GoogleWorkspaceHub
|
|
applications={applications}
|
|
userEmail={userProfile.email}
|
|
userName={userProfile.fullName}
|
|
/>
|
|
)}
|
|
</main>
|
|
|
|
</div>
|
|
|
|
{/* Tailored Resume Inspection Modal */}
|
|
{selectedAppForModal && (
|
|
<TailoredResumeModal
|
|
application={selectedAppForModal}
|
|
onClose={() => setSelectedAppForModal(null)}
|
|
/>
|
|
)}
|
|
|
|
{/* New Application Modal */}
|
|
{isNewAppModalOpen && (
|
|
<NewApplicationModal
|
|
onClose={() => setIsNewAppModalOpen(false)}
|
|
onSaveApplication={handleSaveApplicationRecord}
|
|
/>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
}
|