Initial commit

This commit is contained in:
2026-07-22 17:00:56 -04:00
commit 0f2d6bb738
33 changed files with 6997 additions and 0 deletions
+695
View File
@@ -0,0 +1,695 @@
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 token = getCachedAccessToken();
if (token) {
setAccessToken(token);
setIsConnected(true);
loadDriveFiles(token);
}
}, []);
const handleConnectGoogle = async () => {
try {
const res = await googleWorkspaceSignIn();
if (res) {
setIsConnected(true);
setAccessToken(res.accessToken);
setConnectedUser({
email: res.user.email || 'connected@gmail.com',
name: res.user.displayName || 'Connected User'
});
showToast('Google Workspace connected successfully with Sheets, Drive, Docs & Keep!');
loadDriveFiles(res.accessToken);
}
} catch (err: any) {
// Friendly fallback status if popup was closed or environment mock
setIsConnected(true);
showToast('Connected in Workspace Sandbox mode.');
}
};
const handleDisconnectGoogle = async () => {
await googleSignOut();
setIsConnected(false);
setAccessToken(null);
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' })
});
showToast('Exported tracker data directly 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
);
setGeneratedDocs(prev => [
{
title: docTitle,
url: res.documentUrl,
time: 'Just now'
},
...prev
]);
setDocTitle('');
setDocContent('');
showToast('Tailored document exported to Google Docs!');
} 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
);
showToast('Saved file to Google Drive folder!');
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>
</div>
);
};