809 lines
34 KiB
TypeScript
809 lines
34 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
FileSpreadsheet,
|
|
FileText,
|
|
FolderGit2,
|
|
StickyNote,
|
|
ExternalLink,
|
|
CheckCircle2,
|
|
CloudUpload,
|
|
RefreshCw,
|
|
Sparkles,
|
|
Plus,
|
|
Trash2,
|
|
Lock,
|
|
Download,
|
|
Search,
|
|
Check,
|
|
Zap,
|
|
Globe
|
|
} from 'lucide-react';
|
|
import { JobApplication } from '../types';
|
|
import {
|
|
googleWorkspaceSignIn,
|
|
googleSignOut,
|
|
exportApplicationsToGoogleSheets,
|
|
createTailoredGoogleDoc,
|
|
uploadFileToGoogleDrive,
|
|
listGoogleDriveWorkspaceFiles,
|
|
saveGoogleKeepNote,
|
|
getCachedAccessToken
|
|
} from '../lib/googleAuth';
|
|
|
|
interface GoogleWorkspaceHubProps {
|
|
applications: JobApplication[];
|
|
userEmail?: string;
|
|
userName?: string;
|
|
}
|
|
|
|
export const GoogleWorkspaceHub: React.FC<GoogleWorkspaceHubProps> = ({
|
|
applications,
|
|
userEmail,
|
|
userName
|
|
}) => {
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
|
const [connectedUser, setConnectedUser] = useState<{ email?: string; name?: string }>({
|
|
email: userEmail || 'user@gmail.com',
|
|
name: userName || 'Job Applicant'
|
|
});
|
|
|
|
// State for Google Sheets
|
|
const [isExportingSheets, setIsExportingSheets] = useState(false);
|
|
const [lastSheetExport, setLastSheetExport] = useState<{ url: string; time: string } | null>(null);
|
|
|
|
// State for Google Docs
|
|
const [docType, setDocType] = useState<'resume' | 'coverLetter'>('resume');
|
|
const [docTitle, setDocTitle] = useState('');
|
|
const [docContent, setDocContent] = useState('');
|
|
const [isCreatingDoc, setIsCreatingDoc] = useState(false);
|
|
const [generatedDocs, setGeneratedDocs] = useState<Array<{ title: string; url: string; time: string }>>([]);
|
|
|
|
// State for Google Drive
|
|
const [driveFiles, setDriveFiles] = useState<any[]>([]);
|
|
const [isLoadingDrive, setIsLoadingDrive] = useState(false);
|
|
const [driveSearch, setDriveSearch] = useState('');
|
|
const [uploadFileName, setUploadFileName] = useState('');
|
|
const [uploadFileContent, setUploadFileContent] = useState('');
|
|
const [isUploadingDrive, setIsUploadingDrive] = useState(false);
|
|
|
|
// State for Google Keep
|
|
const [keepNotes, setKeepNotes] = useState<Array<{ id: string; title: string; content: string; color: string; time: string }>>([
|
|
{
|
|
id: 'k1',
|
|
title: 'Senior Frontend Tech Interview Prep',
|
|
content: 'Key focus points: React Fiber architecture, System Design for real-time dashboards, and Google Workspace OAuth flows.',
|
|
color: 'amber',
|
|
time: '10 mins ago'
|
|
},
|
|
{
|
|
id: 'k2',
|
|
title: 'Recruiter Follow-up - Stripe',
|
|
content: 'Email Sarah regarding second round schedule. Confirm availability for Thursday 2 PM PST.',
|
|
color: 'sky',
|
|
time: '2 hours ago'
|
|
}
|
|
]);
|
|
const [newNoteTitle, setNewNoteTitle] = useState('');
|
|
const [newNoteContent, setNewNoteContent] = useState('');
|
|
const [newNoteColor, setNewNoteColor] = useState('amber');
|
|
const [isSavingKeepNote, setIsSavingKeepNote] = useState(false);
|
|
|
|
const [notificationMsg, setNotificationMsg] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const savedConnected = localStorage.getItem('ai_job_portal_google_connected');
|
|
const savedToken = getCachedAccessToken() || localStorage.getItem('ai_job_portal_google_token');
|
|
const savedUser = localStorage.getItem('ai_job_portal_google_user');
|
|
|
|
if (savedConnected === 'true') {
|
|
setIsConnected(true);
|
|
if (savedToken) setAccessToken(savedToken);
|
|
if (savedUser) {
|
|
try { setConnectedUser(JSON.parse(savedUser)); } catch (e) {}
|
|
}
|
|
loadDriveFiles(savedToken || undefined);
|
|
}
|
|
}, []);
|
|
|
|
const [showTokenInputModal, setShowTokenInputModal] = useState(false);
|
|
const [customTokenInput, setCustomTokenInput] = useState('');
|
|
|
|
const handleConnectGoogle = async () => {
|
|
try {
|
|
const res = await googleWorkspaceSignIn();
|
|
if (res) {
|
|
setIsConnected(true);
|
|
setAccessToken(res.accessToken);
|
|
setConnectedUser({
|
|
email: res.user.email || 'dkifer19@gmail.com',
|
|
name: res.user.displayName || 'David Kifer'
|
|
});
|
|
localStorage.setItem('ai_job_portal_google_connected', 'true');
|
|
localStorage.setItem('ai_job_portal_google_token', res.accessToken);
|
|
localStorage.setItem('ai_job_portal_google_user', JSON.stringify({ email: res.user.email || 'dkifer19@gmail.com', name: res.user.displayName || 'David Kifer' }));
|
|
showToast('✓ Google Workspace connected successfully!');
|
|
loadDriveFiles(res.accessToken);
|
|
return;
|
|
}
|
|
} catch (err: any) {
|
|
console.error("Google Auth Popup Exception:", err);
|
|
const msg = err?.message || String(err);
|
|
if (msg.includes('unauthorized-domain')) {
|
|
showToast(`Domain Error: http://localhost:3000 must be added to Firebase Console -> Authentication -> Settings -> Authorized Domains.`);
|
|
} else {
|
|
showToast(`Google Sign-In Error: ${msg}`);
|
|
}
|
|
setShowTokenInputModal(true);
|
|
return;
|
|
}
|
|
};
|
|
|
|
const handleApplyCustomToken = () => {
|
|
if (customTokenInput.trim()) {
|
|
setAccessToken(customTokenInput.trim());
|
|
setIsConnected(true);
|
|
setConnectedUser({ email: 'dkifer19@gmail.com', name: 'David Kifer' });
|
|
showToast('✓ Custom Google OAuth Access Token connected!');
|
|
loadDriveFiles(customTokenInput.trim());
|
|
} else {
|
|
setIsConnected(true);
|
|
setConnectedUser({ email: 'dkifer19@gmail.com', name: 'David Kifer (Sandbox Mode)' });
|
|
showToast('Connected in Offline Workspace Sandbox Mode.');
|
|
}
|
|
setShowTokenInputModal(false);
|
|
};
|
|
|
|
const handleDisconnectGoogle = async () => {
|
|
await googleSignOut();
|
|
setIsConnected(false);
|
|
setAccessToken(null);
|
|
localStorage.removeItem('ai_job_portal_google_connected');
|
|
localStorage.removeItem('ai_job_portal_google_token');
|
|
localStorage.removeItem('ai_job_portal_google_user');
|
|
showToast('Disconnected Google Workspace.');
|
|
};
|
|
|
|
const showToast = (msg: string) => {
|
|
setNotificationMsg(msg);
|
|
setTimeout(() => setNotificationMsg(null), 4000);
|
|
};
|
|
|
|
// 1. Google Sheets Handler
|
|
const handleExportToSheets = async () => {
|
|
setIsExportingSheets(true);
|
|
try {
|
|
const res = await exportApplicationsToGoogleSheets(applications, accessToken || undefined);
|
|
setLastSheetExport({
|
|
url: res.spreadsheetUrl,
|
|
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
});
|
|
|
|
if (res.isLocalCsv && res.spreadsheetUrl) {
|
|
const link = document.createElement('a');
|
|
link.href = res.spreadsheetUrl;
|
|
link.download = `AI_Job_Applications_Tracker_${new Date().toISOString().split('T')[0]}.csv`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
showToast('✓ Applications exported & downloaded to Spreadsheet CSV file!');
|
|
} else {
|
|
showToast(res.message || 'Exported applications to Google Sheets!');
|
|
}
|
|
} catch (err: any) {
|
|
showToast(`Sheets Export: ${err.message || 'Prepared export file'}`);
|
|
} finally {
|
|
setIsExportingSheets(false);
|
|
}
|
|
};
|
|
|
|
// 2. Google Docs Handler
|
|
const handleCreateGoogleDoc = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!docTitle.trim() || !docContent.trim()) return;
|
|
setIsCreatingDoc(true);
|
|
|
|
try {
|
|
const res = await createTailoredGoogleDoc(
|
|
docTitle,
|
|
docContent,
|
|
docType,
|
|
accessToken || undefined
|
|
);
|
|
|
|
if (res.isLocalDoc && res.documentUrl) {
|
|
const link = document.createElement('a');
|
|
link.href = res.documentUrl;
|
|
link.download = `${docTitle.replace(/[^a-z0-9_-]/gi, '_')}.html`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
showToast('✓ Document compiled & downloaded to local file!');
|
|
} else {
|
|
showToast('Tailored document exported to Google Docs!');
|
|
}
|
|
|
|
setGeneratedDocs(prev => [
|
|
{
|
|
title: docTitle,
|
|
url: res.documentUrl,
|
|
time: 'Just now'
|
|
},
|
|
...prev
|
|
]);
|
|
|
|
setDocTitle('');
|
|
setDocContent('');
|
|
} catch (err: any) {
|
|
showToast(`Google Docs: ${err.message}`);
|
|
} finally {
|
|
setIsCreatingDoc(false);
|
|
}
|
|
};
|
|
|
|
// 3. Google Drive Handlers
|
|
const loadDriveFiles = async (token?: string) => {
|
|
setIsLoadingDrive(true);
|
|
try {
|
|
const files = await listGoogleDriveWorkspaceFiles(token || accessToken || undefined);
|
|
setDriveFiles(files);
|
|
} catch (err) {
|
|
console.warn('Drive files load error:', err);
|
|
} finally {
|
|
setIsLoadingDrive(false);
|
|
}
|
|
};
|
|
|
|
const handleUploadDriveFile = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!uploadFileName.trim()) return;
|
|
setIsUploadingDrive(true);
|
|
|
|
try {
|
|
const res = await uploadFileToGoogleDrive(
|
|
uploadFileName,
|
|
uploadFileContent,
|
|
'text/plain',
|
|
accessToken || undefined
|
|
);
|
|
|
|
if (res.isLocalFile && res.driveUrl) {
|
|
const link = document.createElement('a');
|
|
link.href = res.driveUrl;
|
|
link.download = uploadFileName;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
showToast(`✓ ${uploadFileName} saved & downloaded to local workspace!`);
|
|
} else {
|
|
showToast(`✓ ${uploadFileName} uploaded to Google Drive!`);
|
|
}
|
|
|
|
setDriveFiles(prev => [
|
|
{
|
|
id: res.fileId,
|
|
name: uploadFileName,
|
|
webViewLink: res.driveUrl,
|
|
createdTime: new Date().toISOString()
|
|
},
|
|
...prev
|
|
]);
|
|
setUploadFileName('');
|
|
setUploadFileContent('');
|
|
} catch (err: any) {
|
|
showToast(`Drive upload: ${err.message}`);
|
|
} finally {
|
|
setIsUploadingDrive(false);
|
|
}
|
|
};
|
|
|
|
// 4. Google Keep Note Handlers
|
|
const handleSaveKeepNote = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!newNoteTitle.trim() || !newNoteContent.trim()) return;
|
|
setIsSavingKeepNote(true);
|
|
|
|
try {
|
|
const note = await saveGoogleKeepNote(
|
|
newNoteTitle,
|
|
newNoteContent,
|
|
newNoteColor,
|
|
accessToken || undefined
|
|
);
|
|
|
|
setKeepNotes(prev => [
|
|
{
|
|
id: note.noteId,
|
|
title: note.title,
|
|
content: note.content,
|
|
color: note.color,
|
|
time: 'Just now'
|
|
},
|
|
...prev
|
|
]);
|
|
|
|
setNewNoteTitle('');
|
|
setNewNoteContent('');
|
|
showToast('Note synced with Google Keep!');
|
|
} catch (err: any) {
|
|
showToast(`Keep note: ${err.message}`);
|
|
} finally {
|
|
setIsSavingKeepNote(false);
|
|
}
|
|
};
|
|
|
|
const colorClasses: Record<string, string> = {
|
|
amber: 'bg-amber-500/10 border-amber-500/30 text-amber-300',
|
|
sky: 'bg-sky-500/10 border-sky-500/30 text-sky-300',
|
|
emerald: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-300',
|
|
purple: 'bg-purple-500/10 border-purple-500/30 text-purple-300',
|
|
rose: 'bg-rose-500/10 border-rose-500/30 text-rose-300'
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8 pb-12">
|
|
{/* Toast Notification */}
|
|
{notificationMsg && (
|
|
<div className="fixed top-6 right-6 z-50 bg-slate-900 border border-emerald-500/40 text-emerald-300 px-5 py-3.5 rounded-xl shadow-2xl flex items-center space-x-3 text-sm animate-in fade-in slide-in-from-top-4">
|
|
<CheckCircle2 className="w-5 h-5 text-emerald-400 shrink-0" />
|
|
<span>{notificationMsg}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Header Banner */}
|
|
<div className="bg-gradient-to-r from-blue-900/30 via-slate-900 to-indigo-900/30 border border-slate-800 rounded-2xl p-6 md:p-8 backdrop-blur-md relative overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-96 h-96 bg-blue-500/5 rounded-full blur-3xl pointer-events-none" />
|
|
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 relative z-10">
|
|
<div className="space-y-2">
|
|
<div className="inline-flex items-center space-x-2 bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs px-3 py-1 rounded-full font-medium">
|
|
<Globe className="w-3.5 h-3.5" />
|
|
<span>Native Google Workspace OAuth Integration</span>
|
|
</div>
|
|
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 tracking-tight">
|
|
Google Workspace Hub
|
|
</h2>
|
|
<p className="text-slate-400 text-sm max-w-2xl">
|
|
Seamlessly sync application tracking data with Google Sheets, save tailored resumes & cover letters to Google Docs, manage application files in Google Drive, and capture interview notes with Google Keep.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Account Status Card */}
|
|
<div className="bg-slate-900/90 border border-slate-800 rounded-xl p-4 min-w-[280px]">
|
|
<div className="flex items-center space-x-3 mb-3">
|
|
<div className={`w-3 h-3 rounded-full ${isConnected ? 'bg-emerald-400 animate-pulse' : 'bg-slate-600'}`} />
|
|
<span className="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
|
{isConnected ? 'Workspace Connected' : 'Authorization Required'}
|
|
</span>
|
|
</div>
|
|
|
|
{isConnected ? (
|
|
<div className="space-y-3">
|
|
<div className="text-xs text-slate-300">
|
|
<div className="font-semibold text-slate-100">{connectedUser.name}</div>
|
|
<div className="text-slate-400 truncate">{connectedUser.email}</div>
|
|
</div>
|
|
<button
|
|
onClick={handleDisconnectGoogle}
|
|
className="w-full text-xs text-slate-400 hover:text-slate-200 bg-slate-800/80 hover:bg-slate-800 border border-slate-700 py-1.5 px-3 rounded-lg transition-colors"
|
|
>
|
|
Disconnect Workspace
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
<p className="text-xs text-slate-400">
|
|
Enable automatic sync for Sheets, Drive, Docs & Keep.
|
|
</p>
|
|
<button
|
|
onClick={handleConnectGoogle}
|
|
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-medium text-xs py-2 px-3 rounded-lg shadow-lg shadow-blue-600/20 flex items-center justify-center space-x-2 transition-all"
|
|
>
|
|
<Zap className="w-3.5 h-3.5" />
|
|
<span>Connect Google Account</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Grid Section: 4 Apps */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
|
|
{/* APP 1: GOOGLE SHEETS */}
|
|
<div className="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-6">
|
|
<div className="flex items-center justify-between pb-4 border-b border-slate-800">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400">
|
|
<FileSpreadsheet className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-slate-100">Google Sheets Tracker</h3>
|
|
<p className="text-xs text-slate-400">Sync all job submissions to structured spreadsheet</p>
|
|
</div>
|
|
</div>
|
|
<span className="text-xs bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 px-2.5 py-1 rounded-md font-mono">
|
|
v4 REST API
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<p className="text-xs text-slate-300 leading-relaxed">
|
|
Instantly export your live pipeline ({applications.length} applications) into a formatted Google Sheet with frozen headers, automatic status columns, and match scores.
|
|
</p>
|
|
|
|
<div className="bg-slate-950/80 border border-slate-800/80 rounded-xl p-4 space-y-2">
|
|
<div className="flex items-center justify-between text-xs text-slate-400 font-medium">
|
|
<span>Active Tracked Records</span>
|
|
<span className="text-slate-200 font-mono">{applications.length} Jobs</span>
|
|
</div>
|
|
<div className="w-full bg-slate-800 rounded-full h-1.5 overflow-hidden">
|
|
<div className="bg-emerald-500 h-full rounded-full w-full" />
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleExportToSheets}
|
|
disabled={isExportingSheets}
|
|
className="w-full bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white font-medium text-sm py-2.5 rounded-xl shadow-lg shadow-emerald-600/20 flex items-center justify-center space-x-2 transition-all cursor-pointer"
|
|
>
|
|
{isExportingSheets ? (
|
|
<>
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
<span>Generating Google Sheet...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<FileSpreadsheet className="w-4 h-4" />
|
|
<span>Export Live Pipeline to Google Sheets</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
{lastSheetExport && (
|
|
<div className="bg-slate-950/90 border border-emerald-500/30 rounded-xl p-3.5 flex items-center justify-between text-xs">
|
|
<div className="flex items-center space-x-2 text-emerald-300">
|
|
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
|
<span>Exported at {lastSheetExport.time}</span>
|
|
</div>
|
|
<a
|
|
href={lastSheetExport.url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="inline-flex items-center space-x-1 text-emerald-400 hover:text-emerald-300 underline font-medium"
|
|
>
|
|
<span>Open in Sheets</span>
|
|
<ExternalLink className="w-3.5 h-3.5" />
|
|
</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* APP 2: GOOGLE DOCS */}
|
|
<div className="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-6">
|
|
<div className="flex items-center justify-between pb-4 border-b border-slate-800">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="w-10 h-10 rounded-xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-400">
|
|
<FileText className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-slate-100">Google Docs Generator</h3>
|
|
<p className="text-xs text-slate-400">Export tailored resumes & cover letters to Docs</p>
|
|
</div>
|
|
</div>
|
|
<span className="text-xs bg-blue-500/10 border border-blue-500/20 text-blue-400 px-2.5 py-1 rounded-md font-mono">
|
|
Docs API
|
|
</span>
|
|
</div>
|
|
|
|
<form onSubmit={handleCreateGoogleDoc} className="space-y-4">
|
|
<div className="flex items-center space-x-3 bg-slate-950/80 p-1.5 rounded-xl border border-slate-800">
|
|
<button
|
|
type="button"
|
|
onClick={() => setDocType('resume')}
|
|
className={`flex-1 text-xs py-1.5 rounded-lg font-medium transition-all ${
|
|
docType === 'resume' ? 'bg-blue-600 text-white shadow' : 'text-slate-400 hover:text-slate-200'
|
|
}`}
|
|
>
|
|
Tailored Resume
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setDocType('coverLetter')}
|
|
className={`flex-1 text-xs py-1.5 rounded-lg font-medium transition-all ${
|
|
docType === 'coverLetter' ? 'bg-blue-600 text-white shadow' : 'text-slate-400 hover:text-slate-200'
|
|
}`}
|
|
>
|
|
Custom Cover Letter
|
|
</button>
|
|
</div>
|
|
|
|
<div>
|
|
<input
|
|
type="text"
|
|
placeholder={docType === 'resume' ? 'Doc Title (e.g., Senior FullStack Resume - Stripe)' : 'Doc Title (e.g., Cover Letter - Google)'}
|
|
value={docTitle}
|
|
onChange={(e) => setDocTitle(e.target.value)}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<textarea
|
|
rows={3}
|
|
placeholder="Paste or write tailored text content to export to Google Docs..."
|
|
value={docContent}
|
|
onChange={(e) => setDocContent(e.target.value)}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-blue-500 resize-none"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isCreatingDoc || !docTitle || !docContent}
|
|
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-medium text-xs py-2.5 rounded-xl shadow-lg shadow-blue-600/20 flex items-center justify-center space-x-2 transition-all cursor-pointer"
|
|
>
|
|
{isCreatingDoc ? (
|
|
<>
|
|
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
|
<span>Creating Google Doc...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<FileText className="w-3.5 h-3.5" />
|
|
<span>Create & Open in Google Docs</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
|
|
{generatedDocs.length > 0 && (
|
|
<div className="space-y-2 pt-2 border-t border-slate-800">
|
|
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Recently Exported Docs</span>
|
|
<div className="space-y-1.5 max-h-32 overflow-y-auto pr-1">
|
|
{generatedDocs.map((doc, idx) => (
|
|
<div key={idx} className="flex items-center justify-between text-xs bg-slate-950/60 p-2 rounded-lg border border-slate-800">
|
|
<span className="text-slate-200 truncate font-medium max-w-[200px]">{doc.title}</span>
|
|
<a
|
|
href={doc.url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="text-blue-400 hover:text-blue-300 inline-flex items-center space-x-1"
|
|
>
|
|
<span>Open</span>
|
|
<ExternalLink className="w-3 h-3" />
|
|
</a>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* APP 3: GOOGLE DRIVE */}
|
|
<div className="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-6">
|
|
<div className="flex items-center justify-between pb-4 border-b border-slate-800">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="w-10 h-10 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center text-amber-400">
|
|
<FolderGit2 className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-slate-100">Google Drive Vault</h3>
|
|
<p className="text-xs text-slate-400">Upload application packages & manage Drive files</p>
|
|
</div>
|
|
</div>
|
|
<span className="text-xs bg-amber-500/10 border border-amber-500/20 text-amber-400 px-2.5 py-1 rounded-md font-mono">
|
|
Drive v3
|
|
</span>
|
|
</div>
|
|
|
|
<form onSubmit={handleUploadDriveFile} className="space-y-3">
|
|
<input
|
|
type="text"
|
|
placeholder="Artifact Name (e.g., Master_Resume_2026.txt)"
|
|
value={uploadFileName}
|
|
onChange={(e) => setUploadFileName(e.target.value)}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-amber-500"
|
|
/>
|
|
<textarea
|
|
rows={2}
|
|
placeholder="Artifact File Content..."
|
|
value={uploadFileContent}
|
|
onChange={(e) => setUploadFileContent(e.target.value)}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-amber-500 resize-none"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={isUploadingDrive || !uploadFileName}
|
|
className="w-full bg-amber-600 hover:bg-amber-500 disabled:opacity-50 text-white font-medium text-xs py-2 rounded-xl shadow-lg shadow-amber-600/20 flex items-center justify-center space-x-2 transition-all cursor-pointer"
|
|
>
|
|
{isUploadingDrive ? (
|
|
<>
|
|
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
|
<span>Uploading to Google Drive...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<CloudUpload className="w-3.5 h-3.5" />
|
|
<span>Save File to Google Drive</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
|
|
{/* Drive Artifacts List */}
|
|
<div className="space-y-2 pt-2 border-t border-slate-800">
|
|
<div className="flex items-center justify-between text-xs font-semibold text-slate-400">
|
|
<span>Drive Artifacts Vault</span>
|
|
<button
|
|
onClick={() => loadDriveFiles()}
|
|
className="text-slate-400 hover:text-slate-200 flex items-center space-x-1 text-[11px]"
|
|
>
|
|
<RefreshCw className={`w-3 h-3 ${isLoadingDrive ? 'animate-spin' : ''}`} />
|
|
<span>Refresh</span>
|
|
</button>
|
|
</div>
|
|
|
|
{driveFiles.length === 0 ? (
|
|
<div className="bg-slate-950/40 border border-dashed border-slate-800 rounded-xl p-4 text-center text-xs text-slate-500">
|
|
No custom Drive files uploaded yet in this session.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1.5 max-h-36 overflow-y-auto pr-1">
|
|
{driveFiles.map((file) => (
|
|
<div key={file.id} className="flex items-center justify-between text-xs bg-slate-950/60 p-2.5 rounded-lg border border-slate-800">
|
|
<div className="flex items-center space-x-2 overflow-hidden">
|
|
<FolderGit2 className="w-3.5 h-3.5 text-amber-400 shrink-0" />
|
|
<span className="text-slate-200 truncate font-medium">{file.name}</span>
|
|
</div>
|
|
{file.webViewLink && (
|
|
<a
|
|
href={file.webViewLink}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="text-amber-400 hover:text-amber-300 inline-flex items-center space-x-1 shrink-0 ml-2"
|
|
>
|
|
<span>View</span>
|
|
<ExternalLink className="w-3 h-3" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* APP 4: GOOGLE KEEP */}
|
|
<div className="bg-slate-900/60 border border-slate-800 rounded-2xl p-6 space-y-6">
|
|
<div className="flex items-center justify-between pb-4 border-b border-slate-800">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="w-10 h-10 rounded-xl bg-amber-400/10 border border-amber-400/20 flex items-center justify-center text-amber-400">
|
|
<StickyNote className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-slate-100">Google Keep Notes</h3>
|
|
<p className="text-xs text-slate-400">Interview prep & recruiter scratchpad notes</p>
|
|
</div>
|
|
</div>
|
|
<span className="text-xs bg-amber-400/10 border border-amber-400/20 text-amber-400 px-2.5 py-1 rounded-md font-mono">
|
|
Keep API
|
|
</span>
|
|
</div>
|
|
|
|
<form onSubmit={handleSaveKeepNote} className="space-y-3">
|
|
<div className="flex items-center space-x-2">
|
|
<input
|
|
type="text"
|
|
placeholder="Note Title (e.g., Google Team Match Questions)"
|
|
value={newNoteTitle}
|
|
onChange={(e) => setNewNoteTitle(e.target.value)}
|
|
className="flex-1 bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-amber-400"
|
|
/>
|
|
<select
|
|
value={newNoteColor}
|
|
onChange={(e) => setNewNoteColor(e.target.value)}
|
|
className="bg-slate-950 border border-slate-800 text-xs text-slate-300 rounded-xl px-2 py-2 focus:outline-none"
|
|
>
|
|
<option value="amber">Amber</option>
|
|
<option value="sky">Sky Blue</option>
|
|
<option value="emerald">Emerald</option>
|
|
<option value="purple">Purple</option>
|
|
<option value="rose">Rose</option>
|
|
</select>
|
|
</div>
|
|
|
|
<textarea
|
|
rows={2}
|
|
placeholder="Note content, interview answers, or contact details..."
|
|
value={newNoteContent}
|
|
onChange={(e) => setNewNoteContent(e.target.value)}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-amber-400 resize-none"
|
|
/>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isSavingKeepNote || !newNoteTitle}
|
|
className="w-full bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold text-xs py-2 rounded-xl shadow-lg shadow-amber-500/20 flex items-center justify-center space-x-2 transition-all cursor-pointer"
|
|
>
|
|
{isSavingKeepNote ? (
|
|
<>
|
|
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
|
<span>Syncing Note...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<StickyNote className="w-3.5 h-3.5" />
|
|
<span>Save Note to Google Keep</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
|
|
{/* Keep Notes List */}
|
|
<div className="space-y-2 pt-2 border-t border-slate-800">
|
|
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Synced Workspace Notes</span>
|
|
<div className="space-y-2 max-h-40 overflow-y-auto pr-1">
|
|
{keepNotes.map((note) => (
|
|
<div key={note.id} className={`p-3 rounded-xl border ${colorClasses[note.color] || colorClasses.amber} space-y-1`}>
|
|
<div className="flex items-center justify-between text-xs font-semibold">
|
|
<span>{note.title}</span>
|
|
<span className="text-[10px] opacity-70">{note.time}</span>
|
|
</div>
|
|
<p className="text-xs opacity-90 leading-relaxed">{note.content}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Manual OAuth Token Modal */}
|
|
{showTokenInputModal && (
|
|
<div className="fixed inset-0 bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-4 z-50 animate-fadeIn">
|
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl max-w-md w-full p-6 shadow-2xl space-y-4">
|
|
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
|
|
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
|
<Lock className="w-4 h-4 text-indigo-400" /> Google Workspace OAuth Setup
|
|
</h3>
|
|
<button onClick={() => setShowTokenInputModal(false)} className="text-slate-400 hover:text-white text-xs">✕</button>
|
|
</div>
|
|
|
|
<p className="text-xs text-slate-300">
|
|
Google Auth popups require <code className="text-indigo-300">http://localhost:3000</code> to be registered under <strong>Authorized Origins</strong> in your Firebase/Google Cloud console.
|
|
</p>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-xs font-semibold text-slate-400 block">Option A: Paste Google OAuth Access Token</label>
|
|
<input
|
|
type="password"
|
|
placeholder="ya29.a0... (Paste OAuth Token)"
|
|
value={customTokenInput}
|
|
onChange={(e) => setCustomTokenInput(e.target.value)}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-100 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-2 pt-2">
|
|
<button
|
|
onClick={() => handleApplyCustomToken()}
|
|
className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold"
|
|
>
|
|
Use Offline Sandbox
|
|
</button>
|
|
<button
|
|
onClick={() => handleApplyCustomToken()}
|
|
className="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold shadow-lg shadow-indigo-600/30"
|
|
>
|
|
Connect Token
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|