Initial commit
This commit is contained in:
+385
@@ -0,0 +1,385 @@
|
||||
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/firebase';
|
||||
|
||||
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
|
||||
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);
|
||||
} 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user