feat: dark mode for all pages, calendar view for events

This commit is contained in:
2026-01-29 14:12:35 +00:00
parent 8c27b7b522
commit d5706d4ead
16 changed files with 710 additions and 457 deletions

View File

@@ -1,22 +1,23 @@
import { useEffect } from 'react'; import { useEffect, lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import Layout from '@/components/Layout'; import Layout from '@/components/Layout';
import LoginPage from '@/pages/LoginPage';
import DashboardPage from '@/pages/DashboardPage';
import ClientsPage from '@/pages/ClientsPage';
import ClientDetailPage from '@/pages/ClientDetailPage';
import EventsPage from '@/pages/EventsPage';
import EmailsPage from '@/pages/EmailsPage';
import SettingsPage from '@/pages/SettingsPage';
import AdminPage from '@/pages/AdminPage';
import NetworkPage from '@/pages/NetworkPage';
import ReportsPage from '@/pages/ReportsPage';
import InvitePage from '@/pages/InvitePage';
import ForgotPasswordPage from '@/pages/ForgotPasswordPage';
import ResetPasswordPage from '@/pages/ResetPasswordPage';
import { PageLoader } from '@/components/LoadingSpinner'; import { PageLoader } from '@/components/LoadingSpinner';
const LoginPage = lazy(() => import('@/pages/LoginPage'));
const DashboardPage = lazy(() => import('@/pages/DashboardPage'));
const ClientsPage = lazy(() => import('@/pages/ClientsPage'));
const ClientDetailPage = lazy(() => import('@/pages/ClientDetailPage'));
const EventsPage = lazy(() => import('@/pages/EventsPage'));
const EmailsPage = lazy(() => import('@/pages/EmailsPage'));
const SettingsPage = lazy(() => import('@/pages/SettingsPage'));
const AdminPage = lazy(() => import('@/pages/AdminPage'));
const NetworkPage = lazy(() => import('@/pages/NetworkPage'));
const ReportsPage = lazy(() => import('@/pages/ReportsPage'));
const InvitePage = lazy(() => import('@/pages/InvitePage'));
const ForgotPasswordPage = lazy(() => import('@/pages/ForgotPasswordPage'));
const ResetPasswordPage = lazy(() => import('@/pages/ResetPasswordPage'));
function ProtectedRoute({ children }: { children: React.ReactNode }) { function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuthStore(); const { isAuthenticated, isLoading } = useAuthStore();
if (isLoading) return <PageLoader />; if (isLoading) return <PageLoader />;
@@ -33,6 +34,7 @@ export default function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes> <Routes>
<Route path="/login" element={ <Route path="/login" element={
isAuthenticated ? <Navigate to="/" replace /> : <LoginPage /> isAuthenticated ? <Navigate to="/" replace /> : <LoginPage />
@@ -56,6 +58,7 @@ export default function App() {
<Route path="admin" element={<AdminPage />} /> <Route path="admin" element={<AdminPage />} />
</Route> </Route>
</Routes> </Routes>
</Suspense>
</BrowserRouter> </BrowserRouter>
); );
} }

View File

@@ -1,13 +1,13 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const colorMap: Record<string, string> = { const colorMap: Record<string, string> = {
blue: 'bg-blue-50 text-blue-700 border-blue-200', blue: 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/50 dark:text-blue-300 dark:border-blue-800',
green: 'bg-emerald-50 text-emerald-700 border-emerald-200', green: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-900/50 dark:text-emerald-300 dark:border-emerald-800',
yellow: 'bg-amber-50 text-amber-700 border-amber-200', yellow: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/50 dark:text-amber-300 dark:border-amber-800',
red: 'bg-red-50 text-red-700 border-red-200', red: 'bg-red-50 text-red-700 border-red-200 dark:bg-red-900/50 dark:text-red-300 dark:border-red-800',
purple: 'bg-purple-50 text-purple-700 border-purple-200', purple: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/50 dark:text-purple-300 dark:border-purple-800',
gray: 'bg-slate-50 text-slate-600 border-slate-200', gray: 'bg-slate-50 text-slate-600 border-slate-200 dark:bg-slate-700 dark:text-slate-300 dark:border-slate-600',
pink: 'bg-pink-50 text-pink-700 border-pink-200', pink: 'bg-pink-50 text-pink-700 border-pink-200 dark:bg-pink-900/50 dark:text-pink-300 dark:border-pink-800',
}; };
interface BadgeProps { interface BadgeProps {
@@ -26,7 +26,7 @@ export default function Badge({ children, color = 'gray', className, onClick, ac
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border', 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border',
colorMap[color] || colorMap.gray, colorMap[color] || colorMap.gray,
onClick && 'cursor-pointer hover:opacity-80', onClick && 'cursor-pointer hover:opacity-80',
active && 'ring-2 ring-blue-400 ring-offset-1', active && 'ring-2 ring-blue-400 ring-offset-1 dark:ring-offset-slate-900',
className, className,
)} )}
> >

View File

@@ -125,13 +125,13 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
<div className="space-y-4"> <div className="space-y-4">
<div <div
onClick={() => fileRef.current?.click()} onClick={() => fileRef.current?.click()}
className="border-2 border-dashed border-slate-300 rounded-xl p-8 text-center cursor-pointer hover:border-blue-400 hover:bg-blue-50/50 transition-all" className="border-2 border-dashed border-slate-300 dark:border-slate-600 rounded-xl p-8 text-center cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 hover:bg-blue-50/50 dark:hover:bg-blue-900/20 transition-all"
> >
<Upload className="w-10 h-10 text-slate-400 mx-auto mb-3" /> <Upload className="w-10 h-10 text-slate-400 dark:text-slate-500 mx-auto mb-3" />
<p className="text-sm font-medium text-slate-700"> <p className="text-sm font-medium text-slate-700 dark:text-slate-300">
Click to select a CSV file Click to select a CSV file
</p> </p>
<p className="text-xs text-slate-400 mt-1"> <p className="text-xs text-slate-400 dark:text-slate-500 mt-1">
Must include at least First Name and Last Name columns Must include at least First Name and Last Name columns
</p> </p>
</div> </div>
@@ -145,11 +145,11 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
{loading && ( {loading && (
<div className="flex items-center justify-center gap-2 py-4"> <div className="flex items-center justify-center gap-2 py-4">
<LoadingSpinner size="sm" /> <LoadingSpinner size="sm" />
<span className="text-sm text-slate-500">Parsing CSV...</span> <span className="text-sm text-slate-500 dark:text-slate-400">Parsing CSV...</span>
</div> </div>
)} )}
{error && ( {error && (
<div className="flex items-center gap-2 p-3 bg-red-50 text-red-700 rounded-lg text-sm"> <div className="flex items-center gap-2 p-3 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg text-sm">
<AlertCircle className="w-4 h-4 flex-shrink-0" /> <AlertCircle className="w-4 h-4 flex-shrink-0" />
{error} {error}
</div> </div>
@@ -160,25 +160,25 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
{/* Step: Column Mapping */} {/* Step: Column Mapping */}
{step === 'mapping' && preview && ( {step === 'mapping' && preview && (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-2 text-sm text-slate-600"> <div className="flex items-center gap-2 text-sm text-slate-600 dark:text-slate-300">
<FileSpreadsheet className="w-4 h-4" /> <FileSpreadsheet className="w-4 h-4" />
<span><strong>{preview.totalRows}</strong> rows found in <strong>{file?.name}</strong></span> <span><strong>{preview.totalRows}</strong> rows found in <strong>{file?.name}</strong></span>
</div> </div>
{/* Column mapping */} {/* Column mapping */}
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-sm font-medium text-slate-700">Map columns to client fields:</h4> <h4 className="text-sm font-medium text-slate-700 dark:text-slate-300">Map columns to client fields:</h4>
<div className="max-h-64 overflow-y-auto space-y-2"> <div className="max-h-64 overflow-y-auto space-y-2">
{preview.headers.map((header, index) => ( {preview.headers.map((header, index) => (
<div key={index} className="flex items-center gap-3"> <div key={index} className="flex items-center gap-3">
<span className="text-sm text-slate-600 w-40 truncate flex-shrink-0" title={header}> <span className="text-sm text-slate-600 dark:text-slate-300 w-40 truncate flex-shrink-0" title={header}>
{header} {header}
</span> </span>
<ArrowRight className="w-4 h-4 text-slate-300 flex-shrink-0" /> <ArrowRight className="w-4 h-4 text-slate-300 dark:text-slate-600 flex-shrink-0" />
<select <select
value={mapping[index] || ''} value={mapping[index] || ''}
onChange={(e) => updateMapping(index, e.target.value)} onChange={(e) => updateMapping(index, e.target.value)}
className="flex-1 px-3 py-1.5 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className="flex-1 px-3 py-1.5 border border-slate-200 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
{CLIENT_FIELDS.map((f) => ( {CLIENT_FIELDS.map((f) => (
<option key={f.value} value={f.value}>{f.label}</option> <option key={f.value} value={f.value}>{f.label}</option>
@@ -192,27 +192,27 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
{/* Preview table */} {/* Preview table */}
{preview.sampleRows.length > 0 && ( {preview.sampleRows.length > 0 && (
<div> <div>
<h4 className="text-sm font-medium text-slate-700 mb-2">Preview (first {preview.sampleRows.length} rows):</h4> <h4 className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Preview (first {preview.sampleRows.length} rows):</h4>
<div className="overflow-x-auto border border-slate-200 rounded-lg"> <div className="overflow-x-auto border border-slate-200 dark:border-slate-700 rounded-lg">
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead> <thead>
<tr className="bg-slate-50"> <tr className="bg-slate-50 dark:bg-slate-700">
{preview.headers.map((h, i) => ( {preview.headers.map((h, i) => (
<th key={i} className="px-3 py-2 text-left font-medium text-slate-600 whitespace-nowrap"> <th key={i} className="px-3 py-2 text-left font-medium text-slate-600 dark:text-slate-300 whitespace-nowrap">
{mapping[i] ? ( {mapping[i] ? (
<span className="text-blue-600">{CLIENT_FIELDS.find(f => f.value === mapping[i])?.label || mapping[i]}</span> <span className="text-blue-600 dark:text-blue-400">{CLIENT_FIELDS.find(f => f.value === mapping[i])?.label || mapping[i]}</span>
) : ( ) : (
<span className="text-slate-400 line-through">{h}</span> <span className="text-slate-400 dark:text-slate-500 line-through">{h}</span>
)} )}
</th> </th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-100"> <tbody className="divide-y divide-slate-100 dark:divide-slate-700">
{preview.sampleRows.map((row, ri) => ( {preview.sampleRows.map((row, ri) => (
<tr key={ri}> <tr key={ri}>
{row.map((cell, ci) => ( {row.map((cell, ci) => (
<td key={ci} className={`px-3 py-2 whitespace-nowrap ${mapping[ci] ? 'text-slate-700' : 'text-slate-300'}`}> <td key={ci} className={`px-3 py-2 whitespace-nowrap ${mapping[ci] ? 'text-slate-700 dark:text-slate-300' : 'text-slate-300 dark:text-slate-600'}`}>
{cell || '—'} {cell || '—'}
</td> </td>
))} ))}
@@ -225,21 +225,21 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
)} )}
{!hasRequiredFields() && ( {!hasRequiredFields() && (
<div className="flex items-center gap-2 p-3 bg-amber-50 text-amber-700 rounded-lg text-sm"> <div className="flex items-center gap-2 p-3 bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 rounded-lg text-sm">
<AlertCircle className="w-4 h-4 flex-shrink-0" /> <AlertCircle className="w-4 h-4 flex-shrink-0" />
You must map both "First Name" and "Last Name" columns You must map both "First Name" and "Last Name" columns
</div> </div>
)} )}
{error && ( {error && (
<div className="flex items-center gap-2 p-3 bg-red-50 text-red-700 rounded-lg text-sm"> <div className="flex items-center gap-2 p-3 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg text-sm">
<AlertCircle className="w-4 h-4 flex-shrink-0" /> <AlertCircle className="w-4 h-4 flex-shrink-0" />
{error} {error}
</div> </div>
)} )}
<div className="flex justify-between pt-2"> <div className="flex justify-between pt-2">
<button onClick={reset} className="px-4 py-2 text-sm text-slate-600 hover:text-slate-800 transition-colors"> <button onClick={reset} className="px-4 py-2 text-sm text-slate-600 dark:text-slate-300 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
Back Back
</button> </button>
<button <button
@@ -257,19 +257,19 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
{step === 'importing' && ( {step === 'importing' && (
<div className="flex flex-col items-center justify-center py-8 gap-4"> <div className="flex flex-col items-center justify-center py-8 gap-4">
<LoadingSpinner size="lg" /> <LoadingSpinner size="lg" />
<p className="text-sm text-slate-600">Importing clients...</p> <p className="text-sm text-slate-600 dark:text-slate-300">Importing clients...</p>
<p className="text-xs text-slate-400">This may take a moment for large files</p> <p className="text-xs text-slate-400 dark:text-slate-500">This may take a moment for large files</p>
</div> </div>
)} )}
{/* Step: Results */} {/* Step: Results */}
{step === 'results' && result && ( {step === 'results' && result && (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-3 p-4 bg-emerald-50 rounded-lg"> <div className="flex items-center gap-3 p-4 bg-emerald-50 dark:bg-emerald-900/30 rounded-lg">
<CheckCircle2 className="w-8 h-8 text-emerald-600 flex-shrink-0" /> <CheckCircle2 className="w-8 h-8 text-emerald-600 dark:text-emerald-400 flex-shrink-0" />
<div> <div>
<p className="font-semibold text-emerald-800">Import Complete</p> <p className="font-semibold text-emerald-800 dark:text-emerald-300">Import Complete</p>
<p className="text-sm text-emerald-700"> <p className="text-sm text-emerald-700 dark:text-emerald-400">
Successfully imported <strong>{result.imported}</strong> client{result.imported !== 1 ? 's' : ''} Successfully imported <strong>{result.imported}</strong> client{result.imported !== 1 ? 's' : ''}
{result.skipped > 0 && `, ${result.skipped} skipped`} {result.skipped > 0 && `, ${result.skipped} skipped`}
</p> </p>
@@ -278,10 +278,10 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
{result.errors.length > 0 && ( {result.errors.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-sm font-medium text-slate-700">Issues ({result.errors.length}):</h4> <h4 className="text-sm font-medium text-slate-700 dark:text-slate-300">Issues ({result.errors.length}):</h4>
<div className="max-h-40 overflow-y-auto space-y-1 p-3 bg-slate-50 rounded-lg"> <div className="max-h-40 overflow-y-auto space-y-1 p-3 bg-slate-50 dark:bg-slate-700 rounded-lg">
{result.errors.map((err, i) => ( {result.errors.map((err, i) => (
<p key={i} className="text-xs text-slate-600">{err}</p> <p key={i} className="text-xs text-slate-600 dark:text-slate-300">{err}</p>
))} ))}
</div> </div>
</div> </div>

View File

@@ -55,7 +55,6 @@ export default function ClientForm({ initialData, onSubmit, loading }: ClientFor
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
// Clean empty strings
const cleaned = Object.fromEntries( const cleaned = Object.fromEntries(
Object.entries(form).filter(([_, v]) => v !== '' && v !== undefined && !(Array.isArray(v) && v.length === 0)) Object.entries(form).filter(([_, v]) => v !== '' && v !== undefined && !(Array.isArray(v) && v.length === 0))
) as ClientCreate; ) as ClientCreate;
@@ -65,8 +64,8 @@ export default function ClientForm({ initialData, onSubmit, loading }: ClientFor
await onSubmit(cleaned); await onSubmit(cleaned);
}; };
const inputClass = 'w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent'; const inputClass = 'w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent';
const labelClass = 'block text-sm font-medium text-slate-700 mb-1'; const labelClass = 'block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1';
return ( return (
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} className="space-y-5">
@@ -153,15 +152,15 @@ export default function ClientForm({ initialData, onSubmit, loading }: ClientFor
placeholder="Add tag..." placeholder="Add tag..."
className={inputClass} className={inputClass}
/> />
<button type="button" onClick={addTag} className="px-3 py-2 bg-slate-100 hover:bg-slate-200 rounded-lg transition-colors"> <button type="button" onClick={addTag} className="px-3 py-2 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 rounded-lg transition-colors">
<Plus className="w-4 h-4 text-slate-600" /> <Plus className="w-4 h-4 text-slate-600 dark:text-slate-300" />
</button> </button>
</div> </div>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{form.tags?.map((tag) => ( {form.tags?.map((tag) => (
<span key={tag} className="inline-flex items-center gap-1 px-2.5 py-1 bg-blue-50 text-blue-700 rounded-full text-xs"> <span key={tag} className="inline-flex items-center gap-1 px-2.5 py-1 bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded-full text-xs">
{tag} {tag}
<button type="button" onClick={() => removeTag(tag)} className="hover:text-blue-900"><X className="w-3 h-3" /></button> <button type="button" onClick={() => removeTag(tag)} className="hover:text-blue-900 dark:hover:text-blue-100"><X className="w-3 h-3" /></button>
</span> </span>
))} ))}
</div> </div>
@@ -178,15 +177,15 @@ export default function ClientForm({ initialData, onSubmit, loading }: ClientFor
placeholder="Add interest..." placeholder="Add interest..."
className={inputClass} className={inputClass}
/> />
<button type="button" onClick={addInterest} className="px-3 py-2 bg-slate-100 hover:bg-slate-200 rounded-lg transition-colors"> <button type="button" onClick={addInterest} className="px-3 py-2 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 rounded-lg transition-colors">
<Plus className="w-4 h-4 text-slate-600" /> <Plus className="w-4 h-4 text-slate-600 dark:text-slate-300" />
</button> </button>
</div> </div>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{form.interests?.map((i) => ( {form.interests?.map((i) => (
<span key={i} className="inline-flex items-center gap-1 px-2.5 py-1 bg-emerald-50 text-emerald-700 rounded-full text-xs"> <span key={i} className="inline-flex items-center gap-1 px-2.5 py-1 bg-emerald-50 dark:bg-emerald-900/50 text-emerald-700 dark:text-emerald-300 rounded-full text-xs">
{i} {i}
<button type="button" onClick={() => removeInterest(i)} className="hover:text-emerald-900"><X className="w-3 h-3" /></button> <button type="button" onClick={() => removeInterest(i)} className="hover:text-emerald-900 dark:hover:text-emerald-100"><X className="w-3 h-3" /></button>
</span> </span>
))} ))}
</div> </div>

View File

@@ -77,27 +77,29 @@ export default function EmailComposeModal({ isOpen, onClose, clientId, clientNam
setEditContent(''); setEditContent('');
}; };
const inputClass = 'w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500';
return ( return (
<Modal isOpen={isOpen} onClose={() => { onClose(); reset(); }} title={`Email for ${clientName}`} size="lg"> <Modal isOpen={isOpen} onClose={() => { onClose(); reset(); }} title={`Email for ${clientName}`} size="lg">
{!generated ? ( {!generated ? (
<div className="space-y-5"> <div className="space-y-5">
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Purpose / Context</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Purpose / Context</label>
<textarea <textarea
value={purpose} value={purpose}
onChange={(e) => setPurpose(e.target.value)} onChange={(e) => setPurpose(e.target.value)}
rows={3} rows={3}
placeholder="e.g., Follow up after our coffee meeting about the marketing proposal..." placeholder="e.g., Follow up after our coffee meeting about the marketing proposal..."
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className={inputClass}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">AI Provider</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">AI Provider</label>
<select <select
value={provider} value={provider}
onChange={(e) => setProvider(e.target.value as any)} onChange={(e) => setProvider(e.target.value as any)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className={inputClass}
> >
<option value="anthropic">Anthropic (Claude)</option> <option value="anthropic">Anthropic (Claude)</option>
<option value="openai">OpenAI (GPT)</option> <option value="openai">OpenAI (GPT)</option>
@@ -116,7 +118,7 @@ export default function EmailComposeModal({ isOpen, onClose, clientId, clientNam
<button <button
onClick={handleGenerateBirthday} onClick={handleGenerateBirthday}
disabled={isGenerating} disabled={isGenerating}
className="flex items-center gap-2 px-4 py-2.5 bg-pink-50 text-pink-700 rounded-lg text-sm font-medium hover:bg-pink-100 disabled:opacity-50 transition-colors" className="flex items-center gap-2 px-4 py-2.5 bg-pink-50 dark:bg-pink-900/30 text-pink-700 dark:text-pink-300 rounded-lg text-sm font-medium hover:bg-pink-100 dark:hover:bg-pink-900/50 disabled:opacity-50 transition-colors"
> >
<Gift className="w-4 h-4" /> <Gift className="w-4 h-4" />
Birthday Birthday
@@ -126,30 +128,30 @@ export default function EmailComposeModal({ isOpen, onClose, clientId, clientNam
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Subject</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Subject</label>
<input <input
value={editSubject} value={editSubject}
onChange={(e) => setEditSubject(e.target.value)} onChange={(e) => setEditSubject(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className={inputClass}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Content</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Content</label>
<textarea <textarea
value={editContent} value={editContent}
onChange={(e) => setEditContent(e.target.value)} onChange={(e) => setEditContent(e.target.value)}
rows={12} rows={12}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono" className={`${inputClass} font-mono`}
/> />
</div> </div>
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<button onClick={reset} className="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-lg text-sm font-medium transition-colors"> <button onClick={reset} className="px-4 py-2 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg text-sm font-medium transition-colors">
New Email New Email
</button> </button>
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-200 disabled:opacity-50 transition-colors" className="px-4 py-2 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-lg text-sm font-medium hover:bg-slate-200 dark:hover:bg-slate-600 disabled:opacity-50 transition-colors"
> >
{saving ? 'Saving...' : 'Save Draft'} {saving ? 'Saving...' : 'Save Draft'}
</button> </button>

View File

@@ -13,11 +13,11 @@ interface EmptyStateProps {
export default function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) { export default function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
return ( return (
<div className="flex flex-col items-center justify-center py-16 text-center"> <div className="flex flex-col items-center justify-center py-16 text-center">
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mb-4"> <div className="w-16 h-16 bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center mb-4">
<Icon className="w-8 h-8 text-slate-400" /> <Icon className="w-8 h-8 text-slate-400 dark:text-slate-500" />
</div> </div>
<h3 className="text-lg font-semibold text-slate-900 mb-1">{title}</h3> <h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-1">{title}</h3>
<p className="text-sm text-slate-500 max-w-sm mb-6">{description}</p> <p className="text-sm text-slate-500 dark:text-slate-400 max-w-sm mb-6">{description}</p>
{action && ( {action && (
<button <button
onClick={action.onClick} onClick={action.onClick}

View File

@@ -31,16 +31,16 @@ const typeIcons: Record<string, typeof Calendar> = {
}; };
const typeColors: Record<string, string> = { const typeColors: Record<string, string> = {
overdue: 'text-red-500 bg-red-50', overdue: 'text-red-500 bg-red-50 dark:bg-red-900/30',
upcoming: 'text-blue-500 bg-blue-50', upcoming: 'text-blue-500 bg-blue-50 dark:bg-blue-900/30',
stale: 'text-amber-500 bg-amber-50', stale: 'text-amber-500 bg-amber-50 dark:bg-amber-900/30',
drafts: 'text-purple-500 bg-purple-50', drafts: 'text-purple-500 bg-purple-50 dark:bg-purple-900/30',
}; };
const priorityDot: Record<string, string> = { const priorityDot: Record<string, string> = {
high: 'bg-red-500', high: 'bg-red-500',
medium: 'bg-amber-400', medium: 'bg-amber-400',
low: 'bg-slate-300', low: 'bg-slate-300 dark:bg-slate-500',
}; };
export default function NotificationBell() { export default function NotificationBell() {
@@ -52,12 +52,10 @@ export default function NotificationBell() {
useEffect(() => { useEffect(() => {
fetchNotifications(); fetchNotifications();
// Refresh every 5 minutes
const interval = setInterval(fetchNotifications, 5 * 60 * 1000); const interval = setInterval(fetchNotifications, 5 * 60 * 1000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
// Close on outside click
useEffect(() => { useEffect(() => {
const handleClick = (e: MouseEvent) => { const handleClick = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) { if (ref.current && !ref.current.contains(e.target as Node)) {
@@ -92,7 +90,7 @@ export default function NotificationBell() {
onClick={() => setOpen(!open)} onClick={() => setOpen(!open)}
className={cn( className={cn(
'relative p-2 rounded-lg transition-colors', 'relative p-2 rounded-lg transition-colors',
open ? 'bg-slate-100 text-slate-700' : 'text-slate-400 hover:bg-slate-100 hover:text-slate-600' open ? 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-200' : 'text-slate-400 dark:text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-600 dark:hover:text-slate-300'
)} )}
> >
<Bell className="w-5 h-5" /> <Bell className="w-5 h-5" />
@@ -107,13 +105,13 @@ export default function NotificationBell() {
</button> </button>
{open && ( {open && (
<div className="absolute right-0 top-full mt-2 w-80 sm:w-96 bg-white rounded-xl border border-slate-200 shadow-xl z-50 overflow-hidden"> <div className="absolute right-0 top-full mt-2 w-80 sm:w-96 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 shadow-xl z-50 overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100"> <div className="flex items-center justify-between px-4 py-3 border-b border-slate-100 dark:border-slate-700">
<h3 className="text-sm font-semibold text-slate-800">Notifications</h3> <h3 className="text-sm font-semibold text-slate-800 dark:text-slate-200">Notifications</h3>
{counts && ( {counts && (
<div className="flex items-center gap-2 text-xs text-slate-400"> <div className="flex items-center gap-2 text-xs text-slate-400 dark:text-slate-500">
{counts.overdue > 0 && ( {counts.overdue > 0 && (
<span className="text-red-500 font-medium">{counts.overdue} overdue</span> <span className="text-red-500 dark:text-red-400 font-medium">{counts.overdue} overdue</span>
)} )}
{counts.upcoming > 0 && ( {counts.upcoming > 0 && (
<span>{counts.upcoming} upcoming</span> <span>{counts.upcoming} upcoming</span>
@@ -124,7 +122,7 @@ export default function NotificationBell() {
<div className="max-h-[400px] overflow-y-auto"> <div className="max-h-[400px] overflow-y-auto">
{visibleNotifs.length === 0 ? ( {visibleNotifs.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-slate-400"> <div className="flex flex-col items-center justify-center py-8 text-slate-400 dark:text-slate-500">
<Clock className="w-8 h-8 mb-2" /> <Clock className="w-8 h-8 mb-2" />
<p className="text-sm">All caught up!</p> <p className="text-sm">All caught up!</p>
</div> </div>
@@ -132,20 +130,20 @@ export default function NotificationBell() {
visibleNotifs.map(n => { visibleNotifs.map(n => {
const Icon = typeIcons[n.type] || Calendar; const Icon = typeIcons[n.type] || Calendar;
return ( return (
<div key={n.id} className="flex items-start gap-3 px-4 py-3 hover:bg-slate-50 border-b border-slate-50 last:border-0"> <div key={n.id} className="flex items-start gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 border-b border-slate-50 dark:border-slate-700 last:border-0">
<div className={cn('p-1.5 rounded-lg mt-0.5', typeColors[n.type] || 'bg-slate-50 text-slate-500')}> <div className={cn('p-1.5 rounded-lg mt-0.5', typeColors[n.type] || 'bg-slate-50 dark:bg-slate-700 text-slate-500')}>
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
</div> </div>
<Link to={n.link} onClick={() => setOpen(false)} className="flex-1 min-w-0"> <Link to={n.link} onClick={() => setOpen(false)} className="flex-1 min-w-0">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className={cn('w-1.5 h-1.5 rounded-full flex-shrink-0', priorityDot[n.priority])} /> <div className={cn('w-1.5 h-1.5 rounded-full flex-shrink-0', priorityDot[n.priority])} />
<p className="text-sm font-medium text-slate-800 truncate">{n.title}</p> <p className="text-sm font-medium text-slate-800 dark:text-slate-200 truncate">{n.title}</p>
</div> </div>
<p className="text-xs text-slate-400 mt-0.5">{n.description}</p> <p className="text-xs text-slate-400 dark:text-slate-500 mt-0.5">{n.description}</p>
</Link> </Link>
<button <button
onClick={() => dismiss(n.id)} onClick={() => dismiss(n.id)}
className="p-1 rounded text-slate-300 hover:text-slate-500 hover:bg-slate-100 flex-shrink-0" className="p-1 rounded text-slate-300 dark:text-slate-500 hover:text-slate-500 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-600 flex-shrink-0"
> >
<X className="w-3.5 h-3.5" /> <X className="w-3.5 h-3.5" />
</button> </button>
@@ -156,11 +154,11 @@ export default function NotificationBell() {
</div> </div>
{visibleNotifs.length > 0 && ( {visibleNotifs.length > 0 && (
<div className="border-t border-slate-100 px-4 py-2.5"> <div className="border-t border-slate-100 dark:border-slate-700 px-4 py-2.5">
<Link <Link
to="/reports" to="/reports"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
className="text-xs text-blue-600 hover:text-blue-700 font-medium" className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
> >
View Reports View Reports
</Link> </Link>

View File

@@ -137,24 +137,24 @@ export default function AdminPage() {
if (currentUser?.role !== 'admin') { if (currentUser?.role !== 'admin') {
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-red-500 font-medium">Access denied. Admin only.</p> <p className="text-red-500 dark:text-red-400 font-medium">Access denied. Admin only.</p>
</div> </div>
); );
} }
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<h1 className="text-2xl font-bold text-slate-900 mb-6">Admin</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-6">Admin</h1>
{/* Tabs */} {/* Tabs */}
<div className="flex gap-4 border-b border-slate-200 mb-6"> <div className="flex gap-4 border-b border-slate-200 dark:border-slate-700 mb-6">
<button <button
onClick={() => setActiveTab('users')} onClick={() => setActiveTab('users')}
className={cn( className={cn(
'pb-3 px-1 text-sm font-medium border-b-2 transition-colors', 'pb-3 px-1 text-sm font-medium border-b-2 transition-colors',
activeTab === 'users' activeTab === 'users'
? 'border-blue-600 text-blue-600' ? 'border-blue-600 text-blue-600 dark:text-blue-400 dark:border-blue-400'
: 'border-transparent text-slate-500 hover:text-slate-700' : 'border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'
)} )}
> >
<Users className="w-4 h-4 inline mr-2" /> <Users className="w-4 h-4 inline mr-2" />
@@ -165,8 +165,8 @@ export default function AdminPage() {
className={cn( className={cn(
'pb-3 px-1 text-sm font-medium border-b-2 transition-colors', 'pb-3 px-1 text-sm font-medium border-b-2 transition-colors',
activeTab === 'invites' activeTab === 'invites'
? 'border-blue-600 text-blue-600' ? 'border-blue-600 text-blue-600 dark:text-blue-400 dark:border-blue-400'
: 'border-transparent text-slate-500 hover:text-slate-700' : 'border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'
)} )}
> >
<Mail className="w-4 h-4 inline mr-2" /> <Mail className="w-4 h-4 inline mr-2" />
@@ -175,12 +175,12 @@ export default function AdminPage() {
</div> </div>
{isLoading ? ( {isLoading ? (
<div className="text-center py-12 text-slate-500">Loading...</div> <div className="text-center py-12 text-slate-500 dark:text-slate-400">Loading...</div>
) : activeTab === 'users' ? ( ) : activeTab === 'users' ? (
<div className="bg-white rounded-xl border border-slate-200 overflow-x-auto"> <div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-x-auto">
<table className="w-full min-w-[600px]"> <table className="w-full min-w-[600px]">
<thead> <thead>
<tr className="border-b border-slate-200 text-left text-sm text-slate-500"> <tr className="border-b border-slate-200 dark:border-slate-700 text-left text-sm text-slate-500 dark:text-slate-400">
<th className="px-4 py-3 font-medium">Name</th> <th className="px-4 py-3 font-medium">Name</th>
<th className="px-4 py-3 font-medium">Email</th> <th className="px-4 py-3 font-medium">Email</th>
<th className="px-4 py-3 font-medium">Role</th> <th className="px-4 py-3 font-medium">Role</th>
@@ -193,23 +193,23 @@ export default function AdminPage() {
<tr <tr
key={user.id} key={user.id}
onClick={() => setSelectedUser(user)} onClick={() => setSelectedUser(user)}
className="border-b border-slate-100 last:border-0 cursor-pointer hover:bg-slate-50 transition-colors" className="border-b border-slate-100 dark:border-slate-700 last:border-0 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
> >
<td className="px-4 py-3 text-sm font-medium text-slate-900">{user.name}</td> <td className="px-4 py-3 text-sm font-medium text-slate-900 dark:text-slate-100">{user.name}</td>
<td className="px-4 py-3 text-sm text-slate-600">{user.email}</td> <td className="px-4 py-3 text-sm text-slate-600 dark:text-slate-300">{user.email}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={cn( <span className={cn(
'px-2 py-1 text-xs rounded-full', 'px-2 py-1 text-xs rounded-full',
user.role === 'admin' ? 'bg-purple-100 text-purple-700' : 'bg-slate-100 text-slate-700' user.role === 'admin' ? 'bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300' : 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300'
)}> )}>
{user.role} {user.role}
</span> </span>
</td> </td>
<td className="px-4 py-3 text-sm text-slate-500"> <td className="px-4 py-3 text-sm text-slate-500 dark:text-slate-400">
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '—'} {user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '—'}
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<ChevronRight className="w-4 h-4 text-slate-400" /> <ChevronRight className="w-4 h-4 text-slate-400 dark:text-slate-500" />
</td> </td>
</tr> </tr>
))} ))}
@@ -228,29 +228,29 @@ export default function AdminPage() {
Invite User Invite User
</button> </button>
) : ( ) : (
<div className="mb-6 p-4 bg-slate-50 rounded-xl border border-slate-200"> <div className="mb-6 p-4 bg-slate-50 dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700">
<h3 className="font-medium text-slate-900 mb-4">Invite New User</h3> <h3 className="font-medium text-slate-900 dark:text-slate-100 mb-4">Invite New User</h3>
{inviteUrl ? ( {inviteUrl ? (
<div> <div>
<p className="text-sm text-green-600 mb-2"> Invite created! Share this link:</p> <p className="text-sm text-green-600 dark:text-green-400 mb-2"> Invite created! Share this link:</p>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={inviteUrl} value={inviteUrl}
readOnly readOnly
className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm bg-white" className="flex-1 px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100"
/> />
<button <button
onClick={handleCopyUrl} onClick={handleCopyUrl}
className="px-3 py-2 border border-slate-300 rounded-lg hover:bg-slate-100 transition-colors" className="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
> >
{copied ? <Check className="w-4 h-4 text-green-600" /> : <Copy className="w-4 h-4 text-slate-600" />} {copied ? <Check className="w-4 h-4 text-green-600 dark:text-green-400" /> : <Copy className="w-4 h-4 text-slate-600 dark:text-slate-300" />}
</button> </button>
</div> </div>
<button <button
onClick={resetInviteForm} onClick={resetInviteForm}
className="mt-3 text-sm text-slate-500 hover:text-slate-700" className="mt-3 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300"
> >
Create another invite Create another invite
</button> </button>
@@ -258,37 +258,37 @@ export default function AdminPage() {
) : ( ) : (
<form onSubmit={handleCreateInvite} className="space-y-4"> <form onSubmit={handleCreateInvite} className="space-y-4">
{inviteError && ( {inviteError && (
<p className="text-sm text-red-500">{inviteError}</p> <p className="text-sm text-red-500 dark:text-red-400">{inviteError}</p>
)} )}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Name</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Name</label>
<input <input
type="text" type="text"
value={inviteName} value={inviteName}
onChange={(e) => setInviteName(e.target.value)} onChange={(e) => setInviteName(e.target.value)}
required required
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="John Doe" placeholder="John Doe"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Email</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Email</label>
<input <input
type="email" type="email"
value={inviteEmail} value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)} onChange={(e) => setInviteEmail(e.target.value)}
required required
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="john@example.com" placeholder="john@example.com"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Role</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Role</label>
<select <select
value={inviteRole} value={inviteRole}
onChange={(e) => setInviteRole(e.target.value as 'admin' | 'user')} onChange={(e) => setInviteRole(e.target.value as 'admin' | 'user')}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="user">User</option> <option value="user">User</option>
<option value="admin">Admin</option> <option value="admin">Admin</option>
@@ -305,7 +305,7 @@ export default function AdminPage() {
<button <button
type="button" type="button"
onClick={resetInviteForm} onClick={resetInviteForm}
className="px-4 py-2 border border-slate-300 text-sm font-medium rounded-lg hover:bg-slate-100 transition-colors" className="px-4 py-2 border border-slate-300 dark:border-slate-600 text-sm font-medium text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
> >
Cancel Cancel
</button> </button>
@@ -316,10 +316,10 @@ export default function AdminPage() {
)} )}
{/* Invites list */} {/* Invites list */}
<div className="bg-white rounded-xl border border-slate-200 overflow-x-auto"> <div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-x-auto">
<table className="w-full min-w-[600px]"> <table className="w-full min-w-[600px]">
<thead> <thead>
<tr className="border-b border-slate-200 text-left text-sm text-slate-500"> <tr className="border-b border-slate-200 dark:border-slate-700 text-left text-sm text-slate-500 dark:text-slate-400">
<th className="px-4 py-3 font-medium">Name</th> <th className="px-4 py-3 font-medium">Name</th>
<th className="px-4 py-3 font-medium">Email</th> <th className="px-4 py-3 font-medium">Email</th>
<th className="px-4 py-3 font-medium">Role</th> <th className="px-4 py-3 font-medium">Role</th>
@@ -331,19 +331,19 @@ export default function AdminPage() {
<tbody> <tbody>
{invites.length === 0 ? ( {invites.length === 0 ? (
<tr> <tr>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500"> <td colSpan={6} className="px-4 py-8 text-center text-slate-500 dark:text-slate-400">
No invites yet No invites yet
</td> </td>
</tr> </tr>
) : ( ) : (
invites.map((invite) => ( invites.map((invite) => (
<tr key={invite.id} className="border-b border-slate-100 last:border-0"> <tr key={invite.id} className="border-b border-slate-100 dark:border-slate-700 last:border-0">
<td className="px-4 py-3 text-sm font-medium text-slate-900">{invite.name}</td> <td className="px-4 py-3 text-sm font-medium text-slate-900 dark:text-slate-100">{invite.name}</td>
<td className="px-4 py-3 text-sm text-slate-600">{invite.email}</td> <td className="px-4 py-3 text-sm text-slate-600 dark:text-slate-300">{invite.email}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={cn( <span className={cn(
'px-2 py-1 text-xs rounded-full', 'px-2 py-1 text-xs rounded-full',
invite.role === 'admin' ? 'bg-purple-100 text-purple-700' : 'bg-slate-100 text-slate-700' invite.role === 'admin' ? 'bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300' : 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300'
)}> )}>
{invite.role} {invite.role}
</span> </span>
@@ -351,21 +351,21 @@ export default function AdminPage() {
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={cn( <span className={cn(
'px-2 py-1 text-xs rounded-full', 'px-2 py-1 text-xs rounded-full',
invite.status === 'accepted' ? 'bg-green-100 text-green-700' : invite.status === 'accepted' ? 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300' :
invite.status === 'expired' ? 'bg-red-100 text-red-700' : invite.status === 'expired' ? 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300' :
'bg-yellow-100 text-yellow-700' 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300'
)}> )}>
{invite.status} {invite.status}
</span> </span>
</td> </td>
<td className="px-4 py-3 text-sm text-slate-500"> <td className="px-4 py-3 text-sm text-slate-500 dark:text-slate-400">
{new Date(invite.expiresAt).toLocaleDateString()} {new Date(invite.expiresAt).toLocaleDateString()}
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
{invite.status === 'pending' && ( {invite.status === 'pending' && (
<button <button
onClick={() => handleDeleteInvite(invite.id)} onClick={() => handleDeleteInvite(invite.id)}
className="p-1 text-slate-400 hover:text-red-500 transition-colors" className="p-1 text-slate-400 dark:text-slate-500 hover:text-red-500 dark:hover:text-red-400 transition-colors"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
@@ -385,17 +385,17 @@ export default function AdminPage() {
<> <>
{/* Backdrop */} {/* Backdrop */}
<div <div
className="fixed inset-0 bg-black/20 z-40" className="fixed inset-0 bg-black/20 dark:bg-black/50 z-40"
onClick={closeUserPanel} onClick={closeUserPanel}
/> />
{/* Panel */} {/* Panel */}
<div className="fixed inset-y-0 right-0 w-full max-w-md bg-white shadow-xl z-50 flex flex-col animate-in slide-in-from-right duration-200"> <div className="fixed inset-y-0 right-0 w-full max-w-md bg-white dark:bg-slate-800 shadow-xl z-50 flex flex-col animate-in slide-in-from-right duration-200">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-200"> <div className="flex items-center justify-between px-6 py-4 border-b border-slate-200 dark:border-slate-700">
<h2 className="text-lg font-semibold text-slate-900">User Settings</h2> <h2 className="text-lg font-semibold text-slate-900 dark:text-slate-100">User Settings</h2>
<button <button
onClick={closeUserPanel} onClick={closeUserPanel}
className="p-1 text-slate-400 hover:text-slate-600 transition-colors" className="p-1 text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
> >
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
@@ -405,33 +405,33 @@ export default function AdminPage() {
<div className="flex-1 overflow-y-auto p-6 space-y-6"> <div className="flex-1 overflow-y-auto p-6 space-y-6">
{/* User info */} {/* User info */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-semibold text-lg"> <div className="w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center text-blue-600 dark:text-blue-300 font-semibold text-lg">
{selectedUser.name?.charAt(0).toUpperCase() || '?'} {selectedUser.name?.charAt(0).toUpperCase() || '?'}
</div> </div>
<div> <div>
<h3 className="font-medium text-slate-900">{selectedUser.name}</h3> <h3 className="font-medium text-slate-900 dark:text-slate-100">{selectedUser.name}</h3>
<p className="text-sm text-slate-500">{selectedUser.email}</p> <p className="text-sm text-slate-500 dark:text-slate-400">{selectedUser.email}</p>
</div> </div>
</div> </div>
{/* Details */} {/* Details */}
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Joined</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Joined</label>
<p className="text-sm text-slate-600"> <p className="text-sm text-slate-600 dark:text-slate-300">
{selectedUser.createdAt ? new Date(selectedUser.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : '—'} {selectedUser.createdAt ? new Date(selectedUser.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : '—'}
</p> </p>
</div> </div>
{/* Role */} {/* Role */}
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-2">Role</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Role</label>
{selectedUser.id === currentUser?.id ? ( {selectedUser.id === currentUser?.id ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="px-3 py-1.5 text-sm rounded-lg bg-purple-100 text-purple-700 font-medium"> <span className="px-3 py-1.5 text-sm rounded-lg bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300 font-medium">
{selectedUser.role} {selectedUser.role}
</span> </span>
<span className="text-xs text-slate-400">(your account)</span> <span className="text-xs text-slate-400 dark:text-slate-500">(your account)</span>
</div> </div>
) : ( ) : (
<div className="flex gap-2"> <div className="flex gap-2">
@@ -440,8 +440,8 @@ export default function AdminPage() {
className={cn( className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium border transition-colors', 'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium border transition-colors',
selectedUser.role === 'user' selectedUser.role === 'user'
? 'bg-slate-100 border-slate-300 text-slate-900' ? 'bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100'
: 'border-slate-200 text-slate-500 hover:bg-slate-50' : 'border-slate-200 dark:border-slate-600 text-slate-500 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700'
)} )}
> >
<Shield className="w-4 h-4" /> <Shield className="w-4 h-4" />
@@ -452,8 +452,8 @@ export default function AdminPage() {
className={cn( className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium border transition-colors', 'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium border transition-colors',
selectedUser.role === 'admin' selectedUser.role === 'admin'
? 'bg-purple-100 border-purple-300 text-purple-700' ? 'bg-purple-100 dark:bg-purple-900/50 border-purple-300 dark:border-purple-700 text-purple-700 dark:text-purple-300'
: 'border-slate-200 text-slate-500 hover:bg-slate-50' : 'border-slate-200 dark:border-slate-600 text-slate-500 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700'
)} )}
> >
<ShieldAlert className="w-4 h-4" /> <ShieldAlert className="w-4 h-4" />
@@ -466,24 +466,24 @@ export default function AdminPage() {
{/* Actions — only for non-self users */} {/* Actions — only for non-self users */}
{selectedUser.id !== currentUser?.id && ( {selectedUser.id !== currentUser?.id && (
<div className="space-y-4 pt-4 border-t border-slate-200"> <div className="space-y-4 pt-4 border-t border-slate-200 dark:border-slate-700">
<h4 className="text-sm font-medium text-slate-700">Actions</h4> <h4 className="text-sm font-medium text-slate-700 dark:text-slate-300">Actions</h4>
{/* Password Reset */} {/* Password Reset */}
<div className="bg-slate-50 rounded-lg p-4"> <div className="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<KeyRound className="w-4 h-4 text-slate-500" /> <KeyRound className="w-4 h-4 text-slate-500 dark:text-slate-400" />
<span className="text-sm font-medium text-slate-700">Password Reset</span> <span className="text-sm font-medium text-slate-700 dark:text-slate-300">Password Reset</span>
</div> </div>
{resetUrl ? ( {resetUrl ? (
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs text-green-600"> Reset link generated</p> <p className="text-xs text-green-600 dark:text-green-400"> Reset link generated</p>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={resetUrl} value={resetUrl}
readOnly readOnly
className="flex-1 px-3 py-1.5 border border-slate-300 rounded-lg text-xs bg-white font-mono" className="flex-1 px-3 py-1.5 border border-slate-300 dark:border-slate-600 rounded-lg text-xs bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 font-mono"
/> />
<button <button
onClick={handleCopyResetUrl} onClick={handleCopyResetUrl}
@@ -498,7 +498,7 @@ export default function AdminPage() {
<button <button
onClick={() => handleGenerateResetLink(selectedUser.id)} onClick={() => handleGenerateResetLink(selectedUser.id)}
disabled={resetLoading} disabled={resetLoading}
className="text-sm text-blue-600 hover:text-blue-700 font-medium disabled:opacity-50" className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium disabled:opacity-50"
> >
{resetLoading ? 'Generating...' : 'Generate reset link'} {resetLoading ? 'Generating...' : 'Generate reset link'}
</button> </button>
@@ -506,12 +506,12 @@ export default function AdminPage() {
</div> </div>
{/* Delete User */} {/* Delete User */}
<div className="bg-red-50 rounded-lg p-4"> <div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Trash2 className="w-4 h-4 text-red-500" /> <Trash2 className="w-4 h-4 text-red-500 dark:text-red-400" />
<span className="text-sm font-medium text-red-700">Danger Zone</span> <span className="text-sm font-medium text-red-700 dark:text-red-300">Danger Zone</span>
</div> </div>
<p className="text-xs text-red-600 mb-3">Permanently delete this user and all their data.</p> <p className="text-xs text-red-600 dark:text-red-400 mb-3">Permanently delete this user and all their data.</p>
<button <button
onClick={() => handleDeleteUser(selectedUser.id)} onClick={() => handleDeleteUser(selectedUser.id)}
className="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 transition-colors" className="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 transition-colors"

View File

@@ -66,8 +66,8 @@ export default function EmailsPage() {
<div className="max-w-4xl mx-auto space-y-5 animate-fade-in"> <div className="max-w-4xl mx-auto space-y-5 animate-fade-in">
<div className="flex items-center justify-between flex-wrap gap-3"> <div className="flex items-center justify-between flex-wrap gap-3">
<div> <div>
<h1 className="text-2xl font-bold text-slate-900">Emails</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Emails</h1>
<p className="text-slate-500 text-sm mt-1">AI-generated emails for your network</p> <p className="text-slate-500 dark:text-slate-400 text-sm mt-1">AI-generated emails for your network</p>
</div> </div>
<button <button
onClick={() => setShowCompose(true)} onClick={() => setShowCompose(true)}
@@ -87,8 +87,8 @@ export default function EmailsPage() {
className={cn( className={cn(
'px-3 py-1.5 rounded-full text-sm font-medium transition-colors', 'px-3 py-1.5 rounded-full text-sm font-medium transition-colors',
statusFilter === key statusFilter === key
? 'bg-blue-100 text-blue-700' ? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200' : 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600'
)} )}
> >
{label} {label}
@@ -107,22 +107,22 @@ export default function EmailsPage() {
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{filtered.map((email) => ( {filtered.map((email) => (
<div key={email.id} className="bg-white border border-slate-200 rounded-xl p-5"> <div key={email.id} className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-5">
{editingEmail === email.id ? ( {editingEmail === email.id ? (
<div className="space-y-3"> <div className="space-y-3">
<input <input
value={editSubject} value={editSubject}
onChange={(e) => setEditSubject(e.target.value)} onChange={(e) => setEditSubject(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm font-medium text-slate-900 dark:text-slate-100 bg-white dark:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
<textarea <textarea
value={editContent} value={editContent}
onChange={(e) => setEditContent(e.target.value)} onChange={(e) => setEditContent(e.target.value)}
rows={8} rows={8}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono" className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-slate-100 bg-white dark:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono"
/> />
<div className="flex gap-2 justify-end"> <div className="flex gap-2 justify-end">
<button onClick={() => setEditingEmail(null)} className="px-3 py-1.5 text-sm text-slate-600 hover:bg-slate-100 rounded-lg">Cancel</button> <button onClick={() => setEditingEmail(null)} className="px-3 py-1.5 text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg">Cancel</button>
<button onClick={saveEdit} className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">Save</button> <button onClick={saveEdit} className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">Save</button>
</div> </div>
</div> </div>
@@ -133,28 +133,28 @@ export default function EmailsPage() {
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<EmailStatusBadge status={email.status} /> <EmailStatusBadge status={email.status} />
{email.client && ( {email.client && (
<span className="text-xs text-slate-500"> <span className="text-xs text-slate-500 dark:text-slate-400">
To: {email.client.firstName} {email.client.lastName} To: {email.client.firstName} {email.client.lastName}
</span> </span>
)} )}
<span className="text-xs text-slate-400">{formatDate(email.createdAt)}</span> <span className="text-xs text-slate-400 dark:text-slate-500">{formatDate(email.createdAt)}</span>
</div> </div>
<h3 className="font-semibold text-slate-900 mb-2">{email.subject}</h3> <h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-2">{email.subject}</h3>
<p className="text-sm text-slate-600 whitespace-pre-wrap line-clamp-4">{email.content}</p> <p className="text-sm text-slate-600 dark:text-slate-300 whitespace-pre-wrap line-clamp-4">{email.content}</p>
</div> </div>
</div> </div>
<div className="flex gap-2 mt-4 pt-3 border-t border-slate-100"> <div className="flex gap-2 mt-4 pt-3 border-t border-slate-100 dark:border-slate-700">
{email.status === 'draft' && ( {email.status === 'draft' && (
<> <>
<button <button
onClick={() => startEdit(email)} onClick={() => startEdit(email)}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-slate-600 hover:bg-slate-100 rounded-lg transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
> >
<Edit3 className="w-3.5 h-3.5" /> Edit <Edit3 className="w-3.5 h-3.5" /> Edit
</button> </button>
<button <button
onClick={async () => { await sendEmail(email.id); }} onClick={async () => { await sendEmail(email.id); }}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-blue-50 text-blue-700 hover:bg-blue-100 rounded-lg transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/50 rounded-lg transition-colors"
> >
<Send className="w-3.5 h-3.5" /> Send <Send className="w-3.5 h-3.5" /> Send
</button> </button>
@@ -162,7 +162,7 @@ export default function EmailsPage() {
)} )}
<button <button
onClick={async () => { if (confirm('Delete this email?')) await deleteEmail(email.id); }} onClick={async () => { if (confirm('Delete this email?')) await deleteEmail(email.id); }}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors ml-auto" className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-slate-400 dark:text-slate-500 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors ml-auto"
> >
<Trash2 className="w-3.5 h-3.5" /> Delete <Trash2 className="w-3.5 h-3.5" /> Delete
</button> </button>
@@ -178,11 +178,11 @@ export default function EmailsPage() {
<Modal isOpen={showCompose} onClose={() => setShowCompose(false)} title="Generate Email" size="md"> <Modal isOpen={showCompose} onClose={() => setShowCompose(false)} title="Generate Email" size="md">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Client *</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Client *</label>
<select <select
value={composeForm.clientId} value={composeForm.clientId}
onChange={(e) => setComposeForm({ ...composeForm, clientId: e.target.value })} onChange={(e) => setComposeForm({ ...composeForm, clientId: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-slate-100 bg-white dark:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="">Select a client...</option> <option value="">Select a client...</option>
{clients.map((c) => ( {clients.map((c) => (
@@ -191,21 +191,21 @@ export default function EmailsPage() {
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Purpose</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Purpose</label>
<textarea <textarea
value={composeForm.purpose} value={composeForm.purpose}
onChange={(e) => setComposeForm({ ...composeForm, purpose: e.target.value })} onChange={(e) => setComposeForm({ ...composeForm, purpose: e.target.value })}
rows={3} rows={3}
placeholder="What's this email about?" placeholder="What's this email about?"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-slate-100 bg-white dark:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Provider</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Provider</label>
<select <select
value={composeForm.provider} value={composeForm.provider}
onChange={(e) => setComposeForm({ ...composeForm, provider: e.target.value as any })} onChange={(e) => setComposeForm({ ...composeForm, provider: e.target.value as any })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-slate-100 bg-white dark:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="anthropic">Anthropic (Claude)</option> <option value="anthropic">Anthropic (Claude)</option>
<option value="openai">OpenAI (GPT)</option> <option value="openai">OpenAI (GPT)</option>
@@ -223,7 +223,7 @@ export default function EmailsPage() {
<button <button
onClick={handleGenerateBirthday} onClick={handleGenerateBirthday}
disabled={!composeForm.clientId || isGenerating} disabled={!composeForm.clientId || isGenerating}
className="flex items-center gap-2 px-4 py-2.5 bg-pink-50 text-pink-700 rounded-lg text-sm font-medium hover:bg-pink-100 disabled:opacity-50 transition-colors" className="flex items-center gap-2 px-4 py-2.5 bg-pink-50 dark:bg-pink-900/30 text-pink-700 dark:text-pink-300 rounded-lg text-sm font-medium hover:bg-pink-100 dark:hover:bg-pink-900/50 disabled:opacity-50 transition-colors"
> >
<Gift className="w-4 h-4" /> <Gift className="w-4 h-4" />
Birthday Birthday

View File

@@ -1,14 +1,14 @@
import { useEffect, useState } from 'react'; import { useEffect, useState, useMemo } from 'react';
import { useEventsStore } from '@/stores/events'; import { useEventsStore } from '@/stores/events';
import { useClientsStore } from '@/stores/clients'; import { useClientsStore } from '@/stores/clients';
import { Calendar, RefreshCw, Plus, Gift, Heart, Clock, Star } from 'lucide-react'; import { Calendar, RefreshCw, Plus, Gift, Heart, Clock, Star, ChevronLeft, ChevronRight, List, Grid3X3 } from 'lucide-react';
import { cn, formatDate, getDaysUntil } from '@/lib/utils'; import { cn, formatDate, getDaysUntil } from '@/lib/utils';
import Badge, { EventTypeBadge } from '@/components/Badge'; import Badge, { EventTypeBadge } from '@/components/Badge';
import EmptyState from '@/components/EmptyState'; import EmptyState from '@/components/EmptyState';
import { PageLoader } from '@/components/LoadingSpinner'; import { PageLoader } from '@/components/LoadingSpinner';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import LoadingSpinner from '@/components/LoadingSpinner'; import LoadingSpinner from '@/components/LoadingSpinner';
import type { EventCreate } from '@/types'; import type { EventCreate, Event } from '@/types';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
const eventTypes = [ const eventTypes = [
@@ -19,11 +19,179 @@ const eventTypes = [
{ key: 'custom', label: 'Custom', icon: Star }, { key: 'custom', label: 'Custom', icon: Star },
]; ];
const eventTypeColors: Record<string, { dot: string; bg: string; text: string }> = {
birthday: { dot: 'bg-pink-500', bg: 'bg-pink-50 dark:bg-pink-900/30', text: 'text-pink-700 dark:text-pink-300' },
anniversary: { dot: 'bg-purple-500', bg: 'bg-purple-50 dark:bg-purple-900/30', text: 'text-purple-700 dark:text-purple-300' },
followup: { dot: 'bg-blue-500', bg: 'bg-blue-50 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300' },
custom: { dot: 'bg-slate-400', bg: 'bg-slate-50 dark:bg-slate-700', text: 'text-slate-700 dark:text-slate-300' },
};
function getMonthDays(year: number, month: number) {
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const daysInPrevMonth = new Date(year, month, 0).getDate();
const days: { date: number; month: 'prev' | 'current' | 'next'; fullDate: string }[] = [];
// Previous month padding
for (let i = firstDay - 1; i >= 0; i--) {
const d = daysInPrevMonth - i;
const m = month === 0 ? 11 : month - 1;
const y = month === 0 ? year - 1 : year;
days.push({ date: d, month: 'prev', fullDate: `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}` });
}
// Current month
for (let i = 1; i <= daysInMonth; i++) {
days.push({ date: i, month: 'current', fullDate: `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}` });
}
// Next month padding
const remaining = 42 - days.length;
for (let i = 1; i <= remaining; i++) {
const m = month === 11 ? 0 : month + 1;
const y = month === 11 ? year + 1 : year;
days.push({ date: i, month: 'next', fullDate: `${y}-${String(m + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}` });
}
return days;
}
function CalendarView({ events, onSelectDate }: { events: Event[]; onSelectDate: (date: string, evts: Event[]) => void }) {
const today = new Date();
const [year, setYear] = useState(today.getFullYear());
const [month, setMonth] = useState(today.getMonth());
const days = useMemo(() => getMonthDays(year, month), [year, month]);
// Map events to dates (using UTC date part or recurring)
const eventsByDate = useMemo(() => {
const map: Record<string, Event[]> = {};
events.forEach((evt) => {
const d = evt.date.split('T')[0];
// For recurring events, also check same month/day in current year
const evtDate = new Date(d);
const keys = [d];
if (evt.recurring) {
const recurKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(evtDate.getUTCDate()).padStart(2, '0')}`;
if (!keys.includes(recurKey)) keys.push(recurKey);
}
keys.forEach((k) => {
if (!map[k]) map[k] = [];
if (!map[k].includes(evt)) map[k].push(evt);
});
});
return map;
}, [events, year, month]);
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
const prevMonth = () => {
if (month === 0) { setMonth(11); setYear(year - 1); }
else setMonth(month - 1);
};
const nextMonth = () => {
if (month === 11) { setMonth(0); setYear(year + 1); }
else setMonth(month + 1);
};
const goToday = () => { setYear(today.getFullYear()); setMonth(today.getMonth()); };
const monthName = new Date(year, month).toLocaleString('default', { month: 'long', year: 'numeric' });
return (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-5 py-3 border-b border-slate-200 dark:border-slate-700">
<button onClick={prevMonth} className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-600 dark:text-slate-300 transition-colors">
<ChevronLeft className="w-5 h-5" />
</button>
<div className="flex items-center gap-3">
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">{monthName}</h3>
<button onClick={goToday} className="text-xs px-2 py-1 rounded bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">
Today
</button>
</div>
<button onClick={nextMonth} className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-600 dark:text-slate-300 transition-colors">
<ChevronRight className="w-5 h-5" />
</button>
</div>
{/* Day headers */}
<div className="grid grid-cols-7 border-b border-slate-200 dark:border-slate-700">
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((d) => (
<div key={d} className="text-center text-xs font-medium text-slate-500 dark:text-slate-400 py-2">
{d}
</div>
))}
</div>
{/* Day grid */}
<div className="grid grid-cols-7">
{days.map((day, i) => {
const dayEvents = eventsByDate[day.fullDate] || [];
const isToday = day.fullDate === todayStr;
const isCurrentMonth = day.month === 'current';
return (
<button
key={i}
onClick={() => dayEvents.length > 0 && onSelectDate(day.fullDate, dayEvents)}
className={cn(
'relative h-20 sm:h-24 border-b border-r border-slate-100 dark:border-slate-700 p-1.5 text-left transition-colors',
isCurrentMonth ? 'bg-white dark:bg-slate-800' : 'bg-slate-50/50 dark:bg-slate-800/50',
dayEvents.length > 0 && 'cursor-pointer hover:bg-blue-50/50 dark:hover:bg-blue-900/20',
!dayEvents.length && 'cursor-default',
)}
>
<span className={cn(
'inline-flex items-center justify-center w-7 h-7 rounded-full text-sm',
isToday && 'bg-blue-600 text-white font-bold',
!isToday && isCurrentMonth && 'text-slate-900 dark:text-slate-100',
!isToday && !isCurrentMonth && 'text-slate-300 dark:text-slate-600',
)}>
{day.date}
</span>
{dayEvents.length > 0 && (
<div className="flex flex-wrap gap-0.5 mt-0.5">
{dayEvents.slice(0, 3).map((evt, ei) => {
const colors = eventTypeColors[evt.type] || eventTypeColors.custom;
return (
<span key={ei} className={cn('block w-1.5 h-1.5 rounded-full', colors.dot)} title={evt.title} />
);
})}
{dayEvents.length > 3 && (
<span className="text-[9px] text-slate-400 dark:text-slate-500 ml-0.5">+{dayEvents.length - 3}</span>
)}
</div>
)}
{/* Show first event title on larger screens */}
{dayEvents.length > 0 && (
<div className="hidden sm:block mt-0.5">
{dayEvents.slice(0, 2).map((evt, ei) => {
const colors = eventTypeColors[evt.type] || eventTypeColors.custom;
return (
<div key={ei} className={cn('text-[10px] leading-tight px-1 py-0.5 rounded truncate mb-0.5', colors.bg, colors.text)}>
{evt.title}
</div>
);
})}
</div>
)}
</button>
);
})}
</div>
</div>
);
}
export default function EventsPage() { export default function EventsPage() {
const { events, isLoading, typeFilter, setTypeFilter, fetchEvents, createEvent, deleteEvent, syncAll } = useEventsStore(); const { events, isLoading, typeFilter, setTypeFilter, fetchEvents, createEvent, deleteEvent, syncAll } = useEventsStore();
const { clients, fetchClients } = useClientsStore(); const { clients, fetchClients } = useClientsStore();
const [showCreate, setShowCreate] = useState(false); const [showCreate, setShowCreate] = useState(false);
const [syncing, setSyncing] = useState(false); const [syncing, setSyncing] = useState(false);
const [viewMode, setViewMode] = useState<'list' | 'calendar'>('list');
const [selectedDayEvents, setSelectedDayEvents] = useState<{ date: string; events: Event[] } | null>(null);
useEffect(() => { useEffect(() => {
fetchEvents(); fetchEvents();
@@ -53,14 +221,41 @@ export default function EventsPage() {
<div className="max-w-4xl mx-auto space-y-5 animate-fade-in"> <div className="max-w-4xl mx-auto space-y-5 animate-fade-in">
<div className="flex items-center justify-between flex-wrap gap-3"> <div className="flex items-center justify-between flex-wrap gap-3">
<div> <div>
<h1 className="text-2xl font-bold text-slate-900">Events</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Events</h1>
<p className="text-slate-500 text-sm mt-1">{events.length} events tracked</p> <p className="text-slate-500 dark:text-slate-400 text-sm mt-1">{events.length} events tracked</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
{/* View toggle */}
<div className="flex rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<button
onClick={() => setViewMode('list')}
className={cn(
'flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors',
viewMode === 'list'
? 'bg-slate-100 dark:bg-slate-700 text-slate-900 dark:text-slate-100'
: 'bg-white dark:bg-slate-800 text-slate-500 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700'
)}
>
<List className="w-4 h-4" />
List
</button>
<button
onClick={() => setViewMode('calendar')}
className={cn(
'flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors border-l border-slate-200 dark:border-slate-700',
viewMode === 'calendar'
? 'bg-slate-100 dark:bg-slate-700 text-slate-900 dark:text-slate-100'
: 'bg-white dark:bg-slate-800 text-slate-500 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700'
)}
>
<Grid3X3 className="w-4 h-4" />
Calendar
</button>
</div>
<button <button
onClick={handleSync} onClick={handleSync}
disabled={syncing} disabled={syncing}
className="flex items-center gap-2 px-3 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-200 disabled:opacity-50 transition-colors" className="flex items-center gap-2 px-3 py-2 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-lg text-sm font-medium hover:bg-slate-200 dark:hover:bg-slate-600 disabled:opacity-50 transition-colors"
> >
<RefreshCw className={cn('w-4 h-4', syncing && 'animate-spin')} /> <RefreshCw className={cn('w-4 h-4', syncing && 'animate-spin')} />
Sync All Sync All
@@ -84,8 +279,8 @@ export default function EventsPage() {
className={cn( className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium transition-colors', 'flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium transition-colors',
typeFilter === key typeFilter === key
? 'bg-blue-100 text-blue-700' ? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200' : 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600'
)} )}
> >
<Icon className="w-3.5 h-3.5" /> <Icon className="w-3.5 h-3.5" />
@@ -94,7 +289,63 @@ export default function EventsPage() {
))} ))}
</div> </div>
{/* Events list */} {/* Calendar View */}
{viewMode === 'calendar' ? (
<>
<CalendarView
events={filtered}
onSelectDate={(date, evts) => setSelectedDayEvents({ date, events: evts })}
/>
{/* Day detail modal */}
<Modal
isOpen={!!selectedDayEvents}
onClose={() => setSelectedDayEvents(null)}
title={selectedDayEvents ? `Events on ${formatDate(selectedDayEvents.date)}` : ''}
>
{selectedDayEvents && (
<div className="space-y-3">
{selectedDayEvents.events.map((event) => {
const days = getDaysUntil(event.date);
return (
<div key={event.id} className="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-700 rounded-lg">
<div className={cn(
'w-2 h-2 rounded-full flex-shrink-0',
(eventTypeColors[event.type] || eventTypeColors.custom).dot
)} />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">{event.title}</p>
<div className="flex items-center gap-2 mt-0.5">
<EventTypeBadge type={event.type} />
{event.client && (
<Link to={`/clients/${event.clientId}`} className="text-xs text-blue-600 dark:text-blue-400 hover:underline">
{event.client.firstName} {event.client.lastName}
</Link>
)}
</div>
</div>
<button
onClick={async () => {
if (confirm('Delete this event?')) {
await deleteEvent(event.id);
setSelectedDayEvents((prev) =>
prev ? { ...prev, events: prev.events.filter((e) => e.id !== event.id) } : null
);
}
}}
className="p-1.5 rounded text-slate-300 dark:text-slate-500 hover:text-red-500 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors"
>
×
</button>
</div>
);
})}
</div>
)}
</Modal>
</>
) : (
/* List View */
<>
{sorted.length === 0 ? ( {sorted.length === 0 ? (
<EmptyState <EmptyState
icon={Calendar} icon={Calendar}
@@ -103,27 +354,27 @@ export default function EventsPage() {
action={{ label: 'Create Event', onClick: () => setShowCreate(true) }} action={{ label: 'Create Event', onClick: () => setShowCreate(true) }}
/> />
) : ( ) : (
<div className="bg-white border border-slate-200 rounded-xl divide-y divide-slate-100"> <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl divide-y divide-slate-100 dark:divide-slate-700">
{sorted.map((event) => { {sorted.map((event) => {
const days = getDaysUntil(event.date); const days = getDaysUntil(event.date);
return ( return (
<div key={event.id} className="flex items-center gap-4 px-5 py-4"> <div key={event.id} className="flex items-center gap-4 px-5 py-4">
<div className={cn( <div className={cn(
'w-12 h-12 rounded-xl flex flex-col items-center justify-center text-xs font-semibold', 'w-12 h-12 rounded-xl flex flex-col items-center justify-center text-xs font-semibold',
days <= 1 ? 'bg-red-50 text-red-600' : days <= 1 ? 'bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400' :
days <= 7 ? 'bg-amber-50 text-amber-600' : days <= 7 ? 'bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400' :
'bg-slate-50 text-slate-600' 'bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-300'
)}> )}>
<span className="text-lg leading-none">{new Date(event.date).getUTCDate()}</span> <span className="text-lg leading-none">{new Date(event.date).getUTCDate()}</span>
<span className="text-[10px] uppercase">{new Date(event.date).toLocaleString('en', { month: 'short' })}</span> <span className="text-[10px] uppercase">{new Date(event.date).toLocaleString('en', { month: 'short' })}</span>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-900">{event.title}</p> <p className="text-sm font-medium text-slate-900 dark:text-slate-100">{event.title}</p>
<div className="flex items-center gap-2 mt-0.5"> <div className="flex items-center gap-2 mt-0.5">
<EventTypeBadge type={event.type} /> <EventTypeBadge type={event.type} />
{event.recurring && <span className="text-xs text-slate-400">Recurring</span>} {event.recurring && <span className="text-xs text-slate-400 dark:text-slate-500">Recurring</span>}
{event.client && ( {event.client && (
<Link to={`/clients/${event.clientId}`} className="text-xs text-blue-600 hover:underline"> <Link to={`/clients/${event.clientId}`} className="text-xs text-blue-600 dark:text-blue-400 hover:underline">
{event.client.firstName} {event.client.lastName} {event.client.firstName} {event.client.lastName}
</Link> </Link>
)} )}
@@ -132,17 +383,17 @@ export default function EventsPage() {
<div className="text-right"> <div className="text-right">
<p className={cn( <p className={cn(
'text-sm font-medium', 'text-sm font-medium',
days <= 1 ? 'text-red-600' : days <= 7 ? 'text-amber-600' : 'text-slate-500' days <= 1 ? 'text-red-600 dark:text-red-400' : days <= 7 ? 'text-amber-600 dark:text-amber-400' : 'text-slate-500 dark:text-slate-400'
)}> )}>
{days === 0 ? 'Today' : days === 1 ? 'Tomorrow' : `${days} days`} {days === 0 ? 'Today' : days === 1 ? 'Tomorrow' : `${days} days`}
</p> </p>
<p className="text-xs text-slate-400">{formatDate(event.date)}</p> <p className="text-xs text-slate-400 dark:text-slate-500">{formatDate(event.date)}</p>
</div> </div>
<button <button
onClick={async () => { onClick={async () => {
if (confirm('Delete this event?')) await deleteEvent(event.id); if (confirm('Delete this event?')) await deleteEvent(event.id);
}} }}
className="p-1.5 rounded text-slate-300 hover:text-red-500 hover:bg-red-50 transition-colors" className="p-1.5 rounded text-slate-300 dark:text-slate-500 hover:text-red-500 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors"
> >
× ×
</button> </button>
@@ -151,6 +402,8 @@ export default function EventsPage() {
})} })}
</div> </div>
)} )}
</>
)}
{/* Create Modal */} {/* Create Modal */}
<CreateEventModal <CreateEventModal
@@ -195,18 +448,18 @@ function CreateEventModal({ isOpen, onClose, clients, onCreate }: {
} }
}; };
const inputClass = 'w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'; const inputClass = 'w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-slate-100 bg-white dark:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500';
return ( return (
<Modal isOpen={isOpen} onClose={onClose} title="Create Event"> <Modal isOpen={isOpen} onClose={onClose} title="Create Event">
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Title *</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Title *</label>
<input required value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} className={inputClass} /> <input required value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} className={inputClass} />
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Type</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Type</label>
<select value={form.type} onChange={(e) => setForm({ ...form, type: e.target.value as any })} className={inputClass}> <select value={form.type} onChange={(e) => setForm({ ...form, type: e.target.value as any })} className={inputClass}>
<option value="custom">Custom</option> <option value="custom">Custom</option>
<option value="birthday">Birthday</option> <option value="birthday">Birthday</option>
@@ -215,12 +468,12 @@ function CreateEventModal({ isOpen, onClose, clients, onCreate }: {
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Date *</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Date *</label>
<input type="date" required value={form.date} onChange={(e) => setForm({ ...form, date: e.target.value })} className={inputClass} /> <input type="date" required value={form.date} onChange={(e) => setForm({ ...form, date: e.target.value })} className={inputClass} />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Client</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Client</label>
<select value={form.clientId} onChange={(e) => setForm({ ...form, clientId: e.target.value })} className={inputClass}> <select value={form.clientId} onChange={(e) => setForm({ ...form, clientId: e.target.value })} className={inputClass}>
<option value="">None</option> <option value="">None</option>
{clients.map((c) => ( {clients.map((c) => (
@@ -229,22 +482,22 @@ function CreateEventModal({ isOpen, onClose, clients, onCreate }: {
</select> </select>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-sm"> <label className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
<input <input
type="checkbox" type="checkbox"
checked={form.recurring} checked={form.recurring}
onChange={(e) => setForm({ ...form, recurring: e.target.checked })} onChange={(e) => setForm({ ...form, recurring: e.target.checked })}
className="rounded border-slate-300" className="rounded border-slate-300 dark:border-slate-600"
/> />
Recurring annually Recurring annually
</label> </label>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
<label>Remind</label> <label>Remind</label>
<input <input
type="number" type="number"
value={form.reminderDays || ''} value={form.reminderDays || ''}
onChange={(e) => setForm({ ...form, reminderDays: Number(e.target.value) || undefined })} onChange={(e) => setForm({ ...form, reminderDays: Number(e.target.value) || undefined })}
className="w-16 px-2 py-1 border border-slate-300 rounded text-sm" className="w-16 px-2 py-1 border border-slate-300 dark:border-slate-600 rounded text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100"
min={0} min={0}
/> />
<span>days before</span> <span>days before</span>

View File

@@ -26,15 +26,15 @@ export default function ForgotPasswordPage() {
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-slate-50 flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-800 flex items-center justify-center p-4">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
{/* Logo */} {/* Logo */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-14 h-14 bg-blue-600 rounded-2xl mb-4"> <div className="inline-flex items-center justify-center w-14 h-14 bg-blue-600 rounded-2xl mb-4">
<Network className="w-8 h-8 text-white" /> <Network className="w-8 h-8 text-white" />
</div> </div>
<h1 className="text-2xl font-bold text-slate-900">Forgot Password</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Forgot Password</h1>
<p className="text-slate-500 mt-1"> <p className="text-slate-500 dark:text-slate-400 mt-1">
{submitted {submitted
? 'Check your email or contact your admin' ? 'Check your email or contact your admin'
: 'Enter your email to request a password reset'} : 'Enter your email to request a password reset'}
@@ -42,17 +42,17 @@ export default function ForgotPasswordPage() {
</div> </div>
{/* Card */} {/* Card */}
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-8"> <div className="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-8">
{submitted ? ( {submitted ? (
<div className="text-center"> <div className="text-center">
<div className="p-4 bg-green-50 border border-green-200 rounded-lg mb-6"> <div className="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg mb-6">
<p className="text-sm text-green-700"> <p className="text-sm text-green-700 dark:text-green-300">
If an account with <strong>{email}</strong> exists, a password reset has been initiated. Contact your administrator for the reset link. If an account with <strong>{email}</strong> exists, a password reset has been initiated. Contact your administrator for the reset link.
</p> </p>
</div> </div>
<Link <Link
to="/login" to="/login"
className="inline-flex items-center gap-2 text-sm text-blue-600 hover:text-blue-700 font-medium" className="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
Back to login Back to login
@@ -61,19 +61,19 @@ export default function ForgotPasswordPage() {
) : ( ) : (
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} className="space-y-5">
{error && ( {error && (
<div className="p-3 bg-red-50 border border-red-200 text-red-700 text-sm rounded-lg"> <div className="p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 text-sm rounded-lg">
{error} {error}
</div> </div>
)} )}
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Email</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Email</label>
<input <input
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
className="w-full px-3.5 py-2.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow" className="w-full px-3.5 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
placeholder="you@example.com" placeholder="you@example.com"
/> />
</div> </div>
@@ -90,7 +90,7 @@ export default function ForgotPasswordPage() {
<div className="text-center"> <div className="text-center">
<Link <Link
to="/login" to="/login"
className="inline-flex items-center gap-2 text-sm text-slate-500 hover:text-slate-700" className="inline-flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
Back to login Back to login

View File

@@ -51,11 +51,9 @@ export default function InvitePage() {
setSubmitting(true); setSubmitting(true);
try { try {
const result = await api.acceptInvite(token!, password, name); const result = await api.acceptInvite(token!, password, name);
// If we got a token, store it
if (result.token) { if (result.token) {
api.setToken(result.token); api.setToken(result.token);
} }
// Now log in with the credentials
try { try {
await api.login(invite!.email, password); await api.login(invite!.email, password);
} catch { } catch {
@@ -72,7 +70,7 @@ export default function InvitePage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-slate-50"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-800">
<LoadingSpinner size="lg" /> <LoadingSpinner size="lg" />
</div> </div>
); );
@@ -80,58 +78,58 @@ export default function InvitePage() {
if (error) { if (error) {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-slate-50 p-4"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-800 p-4">
<div className="w-full max-w-md text-center"> <div className="w-full max-w-md text-center">
<div className="inline-flex items-center justify-center w-14 h-14 bg-red-100 rounded-2xl mb-4"> <div className="inline-flex items-center justify-center w-14 h-14 bg-red-100 dark:bg-red-900/50 rounded-2xl mb-4">
<Network className="w-8 h-8 text-red-500" /> <Network className="w-8 h-8 text-red-500 dark:text-red-400" />
</div> </div>
<h1 className="text-2xl font-bold text-slate-900 mb-2">Invalid Invite</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">Invalid Invite</h1>
<p className="text-slate-500">{error}</p> <p className="text-slate-500 dark:text-slate-400">{error}</p>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-slate-50 flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-800 flex items-center justify-center p-4">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
{/* Logo */} {/* Logo */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-14 h-14 bg-blue-600 rounded-2xl mb-4"> <div className="inline-flex items-center justify-center w-14 h-14 bg-blue-600 rounded-2xl mb-4">
<Network className="w-8 h-8 text-white" /> <Network className="w-8 h-8 text-white" />
</div> </div>
<h1 className="text-2xl font-bold text-slate-900">Welcome to NetworkCRM</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Welcome to NetworkCRM</h1>
<p className="text-slate-500 mt-1">You've been invited to join as <span className="font-medium text-slate-700">{invite?.role}</span></p> <p className="text-slate-500 dark:text-slate-400 mt-1">You've been invited to join as <span className="font-medium text-slate-700 dark:text-slate-300">{invite?.role}</span></p>
</div> </div>
{/* Card */} {/* Card */}
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-8"> <div className="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-8">
<div className="mb-6 p-3 bg-blue-50 rounded-lg"> <div className="mb-6 p-3 bg-blue-50 dark:bg-blue-900/30 rounded-lg">
<p className="text-sm text-blue-700"> <p className="text-sm text-blue-700 dark:text-blue-300">
Setting up account for <strong>{invite?.email}</strong> Setting up account for <strong>{invite?.email}</strong>
</p> </p>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} className="space-y-5">
{submitError && ( {submitError && (
<div className="p-3 bg-red-50 border border-red-200 text-red-700 text-sm rounded-lg"> <div className="p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 text-sm rounded-lg">
{submitError} {submitError}
</div> </div>
)} )}
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Name</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Name</label>
<input <input
type="text" type="text"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
required required
className="w-full px-3.5 py-2.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow" className="w-full px-3.5 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Password</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Password</label>
<div className="relative"> <div className="relative">
<input <input
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
@@ -139,13 +137,13 @@ export default function InvitePage() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
minLength={8} minLength={8}
className="w-full px-3.5 py-2.5 pr-10 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow" className="w-full px-3.5 py-2.5 pr-10 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
placeholder="Min 8 characters" placeholder="Min 8 characters"
/> />
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600" className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300"
> >
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />} {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button> </button>
@@ -153,13 +151,13 @@ export default function InvitePage() {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Confirm Password</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Confirm Password</label>
<input <input
type="password" type="password"
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
required required
className="w-full px-3.5 py-2.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow" className="w-full px-3.5 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
placeholder="Re-enter password" placeholder="Re-enter password"
/> />
</div> </div>

View File

@@ -9,19 +9,19 @@ import {
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { PageLoader } from '@/components/LoadingSpinner'; import { PageLoader } from '@/components/LoadingSpinner';
const categoryConfig: Record<string, { label: string; icon: typeof Network; color: string; bg: string }> = { const categoryConfig: Record<string, { label: string; icon: typeof Network; color: string; darkColor: string; bg: string; darkBg: string }> = {
industry: { label: 'Industry', icon: Building2, color: 'text-blue-600', bg: 'bg-blue-50' }, industry: { label: 'Industry', icon: Building2, color: 'text-blue-600', darkColor: 'dark:text-blue-400', bg: 'bg-blue-50', darkBg: 'dark:bg-blue-900/30' },
interests: { label: 'Interests', icon: Heart, color: 'text-pink-600', bg: 'bg-pink-50' }, interests: { label: 'Interests', icon: Heart, color: 'text-pink-600', darkColor: 'dark:text-pink-400', bg: 'bg-pink-50', darkBg: 'dark:bg-pink-900/30' },
location: { label: 'Location', icon: MapPin, color: 'text-emerald-600', bg: 'bg-emerald-50' }, location: { label: 'Location', icon: MapPin, color: 'text-emerald-600', darkColor: 'dark:text-emerald-400', bg: 'bg-emerald-50', darkBg: 'dark:bg-emerald-900/30' },
business: { label: 'Business', icon: Briefcase, color: 'text-purple-600', bg: 'bg-purple-50' }, business: { label: 'Business', icon: Briefcase, color: 'text-purple-600', darkColor: 'dark:text-purple-400', bg: 'bg-purple-50', darkBg: 'dark:bg-purple-900/30' },
social: { label: 'Social', icon: Users, color: 'text-amber-600', bg: 'bg-amber-50' }, social: { label: 'Social', icon: Users, color: 'text-amber-600', darkColor: 'dark:text-amber-400', bg: 'bg-amber-50', darkBg: 'dark:bg-amber-900/30' },
}; };
function ScoreBadge({ score }: { score: number }) { function ScoreBadge({ score }: { score: number }) {
const color = score >= 60 ? 'bg-emerald-100 text-emerald-700' : const color = score >= 60 ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300' :
score >= 40 ? 'bg-blue-100 text-blue-700' : score >= 40 ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300' :
score >= 25 ? 'bg-amber-100 text-amber-700' : score >= 25 ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300' :
'bg-slate-100 text-slate-600'; 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300';
return ( return (
<span className={cn('px-2 py-0.5 rounded-full text-xs font-bold', color)}> <span className={cn('px-2 py-0.5 rounded-full text-xs font-bold', color)}>
{score}% {score}%
@@ -38,10 +38,10 @@ function MatchCard({ match, onGenerateIntro, generatingIntro }: {
const Icon = config.icon; const Icon = config.icon;
return ( return (
<div className="bg-white border border-slate-200 rounded-xl p-5 hover:shadow-md transition-shadow"> <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-5 hover:shadow-md dark:hover:shadow-slate-900/50 transition-shadow">
{/* Header */} {/* Header */}
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div className={cn('flex items-center gap-2 px-2.5 py-1 rounded-full text-xs font-medium', config.bg, config.color)}> <div className={cn('flex items-center gap-2 px-2.5 py-1 rounded-full text-xs font-medium', config.bg, config.darkBg, config.color, config.darkColor)}>
<Icon className="w-3.5 h-3.5" /> <Icon className="w-3.5 h-3.5" />
{config.label} {config.label}
</div> </div>
@@ -52,40 +52,40 @@ function MatchCard({ match, onGenerateIntro, generatingIntro }: {
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<Link <Link
to={`/clients/${match.clientA.id}`} to={`/clients/${match.clientA.id}`}
className="flex-1 flex items-center gap-2 p-2 rounded-lg hover:bg-slate-50 transition-colors min-w-0" className="flex-1 flex items-center gap-2 p-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors min-w-0"
> >
<div className="w-9 h-9 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center text-sm font-semibold flex-shrink-0"> <div className="w-9 h-9 bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded-full flex items-center justify-center text-sm font-semibold flex-shrink-0">
{match.clientA.name.split(' ').map(n => n[0]).join('')} {match.clientA.name.split(' ').map(n => n[0]).join('')}
</div> </div>
<span className="text-sm font-medium text-slate-900 truncate">{match.clientA.name}</span> <span className="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">{match.clientA.name}</span>
</Link> </Link>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Network className="w-4 h-4 text-slate-300" /> <Network className="w-4 h-4 text-slate-300 dark:text-slate-600" />
</div> </div>
<Link <Link
to={`/clients/${match.clientB.id}`} to={`/clients/${match.clientB.id}`}
className="flex-1 flex items-center gap-2 p-2 rounded-lg hover:bg-slate-50 transition-colors min-w-0" className="flex-1 flex items-center gap-2 p-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors min-w-0"
> >
<div className="w-9 h-9 bg-purple-100 text-purple-700 rounded-full flex items-center justify-center text-sm font-semibold flex-shrink-0"> <div className="w-9 h-9 bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300 rounded-full flex items-center justify-center text-sm font-semibold flex-shrink-0">
{match.clientB.name.split(' ').map(n => n[0]).join('')} {match.clientB.name.split(' ').map(n => n[0]).join('')}
</div> </div>
<span className="text-sm font-medium text-slate-900 truncate">{match.clientB.name}</span> <span className="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">{match.clientB.name}</span>
</Link> </Link>
</div> </div>
{/* Reasons */} {/* Reasons */}
<div className="space-y-1.5 mb-3"> <div className="space-y-1.5 mb-3">
{match.reasons.map((reason, i) => ( {match.reasons.map((reason, i) => (
<div key={i} className="flex items-start gap-2 text-sm text-slate-600"> <div key={i} className="flex items-start gap-2 text-sm text-slate-600 dark:text-slate-300">
<span className="text-emerald-500 mt-0.5 flex-shrink-0"></span> <span className="text-emerald-500 dark:text-emerald-400 mt-0.5 flex-shrink-0"></span>
{reason} {reason}
</div> </div>
))} ))}
</div> </div>
{/* Intro suggestion */} {/* Intro suggestion */}
<div className="bg-slate-50 rounded-lg p-3 mb-3"> <div className="bg-slate-50 dark:bg-slate-700 rounded-lg p-3 mb-3">
<p className="text-sm text-slate-600 italic">{match.introSuggestion}</p> <p className="text-sm text-slate-600 dark:text-slate-300 italic">{match.introSuggestion}</p>
</div> </div>
{/* Actions */} {/* Actions */}
@@ -93,7 +93,7 @@ function MatchCard({ match, onGenerateIntro, generatingIntro }: {
<button <button
onClick={() => onGenerateIntro(match)} onClick={() => onGenerateIntro(match)}
disabled={generatingIntro} disabled={generatingIntro}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-blue-50 text-blue-700 hover:bg-blue-100 rounded-lg transition-colors disabled:opacity-50" className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/50 rounded-lg transition-colors disabled:opacity-50"
> >
{generatingIntro ? ( {generatingIntro ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" /> <Loader2 className="w-3.5 h-3.5 animate-spin" />
@@ -165,8 +165,8 @@ export default function NetworkPage() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between flex-wrap gap-3"> <div className="flex items-center justify-between flex-wrap gap-3">
<div> <div>
<h1 className="text-2xl font-bold text-slate-900">Network Matching</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Network Matching</h1>
<p className="text-slate-500 text-sm mt-1"> <p className="text-slate-500 dark:text-slate-400 text-sm mt-1">
AI-powered suggestions for connecting your clients AI-powered suggestions for connecting your clients
</p> </p>
</div> </div>
@@ -175,33 +175,33 @@ export default function NetworkPage() {
{/* Stats */} {/* Stats */}
{stats && ( {stats && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="bg-white border border-slate-200 rounded-xl p-4"> <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Users className="w-4 h-4 text-blue-500" /> <Users className="w-4 h-4 text-blue-500" />
<span className="text-sm text-slate-500">Clients</span> <span className="text-sm text-slate-500 dark:text-slate-400">Clients</span>
</div> </div>
<p className="text-2xl font-bold text-slate-900">{stats.totalClients}</p> <p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{stats.totalClients}</p>
</div> </div>
<div className="bg-white border border-slate-200 rounded-xl p-4"> <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Network className="w-4 h-4 text-purple-500" /> <Network className="w-4 h-4 text-purple-500" />
<span className="text-sm text-slate-500">Matches Found</span> <span className="text-sm text-slate-500 dark:text-slate-400">Matches Found</span>
</div> </div>
<p className="text-2xl font-bold text-slate-900">{stats.totalMatches}</p> <p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{stats.totalMatches}</p>
</div> </div>
<div className="bg-white border border-slate-200 rounded-xl p-4"> <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<TrendingUp className="w-4 h-4 text-emerald-500" /> <TrendingUp className="w-4 h-4 text-emerald-500" />
<span className="text-sm text-slate-500">Avg Score</span> <span className="text-sm text-slate-500 dark:text-slate-400">Avg Score</span>
</div> </div>
<p className="text-2xl font-bold text-slate-900">{stats.avgScore}%</p> <p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{stats.avgScore}%</p>
</div> </div>
<div className="bg-white border border-slate-200 rounded-xl p-4"> <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Star className="w-4 h-4 text-amber-500" /> <Star className="w-4 h-4 text-amber-500" />
<span className="text-sm text-slate-500">Top Connector</span> <span className="text-sm text-slate-500 dark:text-slate-400">Top Connector</span>
</div> </div>
<p className="text-lg font-bold text-slate-900 truncate"> <p className="text-lg font-bold text-slate-900 dark:text-slate-100 truncate">
{stats.topConnectors[0]?.name || '—'} {stats.topConnectors[0]?.name || '—'}
</p> </p>
</div> </div>
@@ -210,20 +210,20 @@ export default function NetworkPage() {
{/* Top Connectors */} {/* Top Connectors */}
{stats && stats.topConnectors.length > 0 && ( {stats && stats.topConnectors.length > 0 && (
<div className="bg-white border border-slate-200 rounded-xl p-5"> <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-5">
<h3 className="font-semibold text-slate-900 mb-3">Most Connected Clients</h3> <h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-3">Most Connected Clients</h3>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{stats.topConnectors.map((c) => ( {stats.topConnectors.map((c) => (
<Link <Link
key={c.id} key={c.id}
to={`/clients/${c.id}`} to={`/clients/${c.id}`}
className="flex items-center gap-2 px-3 py-2 bg-slate-50 hover:bg-slate-100 rounded-lg transition-colors" className="flex items-center gap-2 px-3 py-2 bg-slate-50 dark:bg-slate-700 hover:bg-slate-100 dark:hover:bg-slate-600 rounded-lg transition-colors"
> >
<div className="w-7 h-7 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center text-xs font-semibold"> <div className="w-7 h-7 bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded-full flex items-center justify-center text-xs font-semibold">
{c.name.split(' ').map(n => n[0]).join('')} {c.name.split(' ').map(n => n[0]).join('')}
</div> </div>
<span className="text-sm font-medium text-slate-700">{c.name}</span> <span className="text-sm font-medium text-slate-700 dark:text-slate-300">{c.name}</span>
<span className="text-xs text-slate-400">{c.matchCount} matches</span> <span className="text-xs text-slate-400 dark:text-slate-500">{c.matchCount} matches</span>
</Link> </Link>
))} ))}
</div> </div>
@@ -232,7 +232,7 @@ export default function NetworkPage() {
{/* Filters */} {/* Filters */}
<div className="flex items-center gap-3 flex-wrap"> <div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-1.5 text-sm text-slate-500"> <div className="flex items-center gap-1.5 text-sm text-slate-500 dark:text-slate-400">
<Filter className="w-4 h-4" /> <Filter className="w-4 h-4" />
Filter: Filter:
</div> </div>
@@ -240,7 +240,7 @@ export default function NetworkPage() {
onClick={() => setCategoryFilter(null)} onClick={() => setCategoryFilter(null)}
className={cn( className={cn(
'px-3 py-1.5 rounded-full text-sm font-medium transition-colors', 'px-3 py-1.5 rounded-full text-sm font-medium transition-colors',
!categoryFilter ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200' !categoryFilter ? 'bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900' : 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600'
)} )}
> >
All ({matches.length}) All ({matches.length})
@@ -255,7 +255,7 @@ export default function NetworkPage() {
onClick={() => setCategoryFilter(categoryFilter === cat ? null : cat)} onClick={() => setCategoryFilter(categoryFilter === cat ? null : cat)}
className={cn( className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium transition-colors', 'flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium transition-colors',
categoryFilter === cat ? cn(config.bg, config.color, 'ring-2 ring-offset-1', `ring-current`) : 'bg-slate-100 text-slate-600 hover:bg-slate-200' categoryFilter === cat ? cn(config.bg, config.darkBg, config.color, config.darkColor, 'ring-2 ring-offset-1 dark:ring-offset-slate-900', `ring-current`) : 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600'
)} )}
> >
{config.label} ({count}) {config.label} ({count})
@@ -263,11 +263,11 @@ export default function NetworkPage() {
); );
})} })}
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">
<label className="text-xs text-slate-500">Min score:</label> <label className="text-xs text-slate-500 dark:text-slate-400">Min score:</label>
<select <select
value={minScore} value={minScore}
onChange={(e) => setMinScore(parseInt(e.target.value))} onChange={(e) => setMinScore(parseInt(e.target.value))}
className="text-sm border border-slate-200 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-200" className="text-sm border border-slate-200 dark:border-slate-600 rounded-lg px-2 py-1 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-200"
> >
<option value={10}>10%</option> <option value={10}>10%</option>
<option value={20}>20%</option> <option value={20}>20%</option>
@@ -279,10 +279,10 @@ export default function NetworkPage() {
{/* Match Cards */} {/* Match Cards */}
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<div className="bg-white border border-slate-200 rounded-xl p-12 text-center"> <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-12 text-center">
<Network className="w-12 h-12 text-slate-300 mx-auto mb-3" /> <Network className="w-12 h-12 text-slate-300 dark:text-slate-600 mx-auto mb-3" />
<h3 className="font-semibold text-slate-900 mb-1">No matches found</h3> <h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">No matches found</h3>
<p className="text-sm text-slate-500 mb-4"> <p className="text-sm text-slate-500 dark:text-slate-400 mb-4">
{matches.length === 0 {matches.length === 0
? 'Add more clients with detailed profiles (interests, industry, location) to find connections.' ? 'Add more clients with detailed profiles (interests, industry, location) to find connections.'
: 'Try adjusting the filter or minimum score.'} : 'Try adjusting the filter or minimum score.'}

View File

@@ -39,14 +39,14 @@ function StatCard({ icon: Icon, label, value, sub, color }: {
icon: typeof Users; label: string; value: number | string; sub?: string; color: string; icon: typeof Users; label: string; value: number | string; sub?: string; color: string;
}) { }) {
return ( return (
<div className="bg-white rounded-xl border border-slate-200 p-5 flex items-start gap-4"> <div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5 flex items-start gap-4">
<div className={cn('p-2.5 rounded-xl', color)}> <div className={cn('p-2.5 rounded-xl', color)}>
<Icon className="w-5 h-5" /> <Icon className="w-5 h-5" />
</div> </div>
<div> <div>
<p className="text-2xl font-bold text-slate-900">{value}</p> <p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{value}</p>
<p className="text-sm text-slate-500">{label}</p> <p className="text-sm text-slate-500 dark:text-slate-400">{label}</p>
{sub && <p className="text-xs text-slate-400 mt-0.5">{sub}</p>} {sub && <p className="text-xs text-slate-400 dark:text-slate-500 mt-0.5">{sub}</p>}
</div> </div>
</div> </div>
); );
@@ -59,17 +59,17 @@ function BarChartSimple({ data, label, color }: {
}) { }) {
const max = Math.max(...data.map(d => d.value), 1); const max = Math.max(...data.map(d => d.value), 1);
return ( return (
<div className="bg-white rounded-xl border border-slate-200 p-5"> <div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5">
<h3 className="text-sm font-semibold text-slate-700 mb-4">{label}</h3> <h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-4">{label}</h3>
<div className="flex items-end gap-1.5 h-32"> <div className="flex items-end gap-1.5 h-32">
{data.map((d, i) => ( {data.map((d, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-1"> <div key={i} className="flex-1 flex flex-col items-center gap-1">
<span className="text-[10px] text-slate-500 font-medium">{d.value || ''}</span> <span className="text-[10px] text-slate-500 dark:text-slate-400 font-medium">{d.value || ''}</span>
<div <div
className={cn('w-full rounded-t transition-all', color)} className={cn('w-full rounded-t transition-all', color)}
style={{ height: `${Math.max((d.value / max) * 100, 2)}%`, minHeight: d.value > 0 ? 4 : 2 }} style={{ height: `${Math.max((d.value / max) * 100, 2)}%`, minHeight: d.value > 0 ? 4 : 2 }}
/> />
<span className="text-[10px] text-slate-400 truncate w-full text-center"> <span className="text-[10px] text-slate-400 dark:text-slate-500 truncate w-full text-center">
{d.label} {d.label}
</span> </span>
</div> </div>
@@ -83,15 +83,15 @@ function EngagementRing({ summary }: { summary: EngagementData['summary'] }) {
const total = summary.engaged + summary.warm + summary.cooling + summary.cold; const total = summary.engaged + summary.warm + summary.cooling + summary.cold;
if (total === 0) return null; if (total === 0) return null;
const segments = [ const segments = [
{ label: 'Engaged', count: summary.engaged, color: 'bg-emerald-500', textColor: 'text-emerald-600', icon: Flame }, { label: 'Engaged', count: summary.engaged, color: 'bg-emerald-500', textColor: 'text-emerald-600 dark:text-emerald-400', icon: Flame },
{ label: 'Warm', count: summary.warm, color: 'bg-amber-400', textColor: 'text-amber-600', icon: ThermometerSun }, { label: 'Warm', count: summary.warm, color: 'bg-amber-400', textColor: 'text-amber-600 dark:text-amber-400', icon: ThermometerSun },
{ label: 'Cooling', count: summary.cooling, color: 'bg-blue-400', textColor: 'text-blue-600', icon: Activity }, { label: 'Cooling', count: summary.cooling, color: 'bg-blue-400', textColor: 'text-blue-600 dark:text-blue-400', icon: Activity },
{ label: 'Cold', count: summary.cold, color: 'bg-slate-300', textColor: 'text-slate-500', icon: Snowflake }, { label: 'Cold', count: summary.cold, color: 'bg-slate-300 dark:bg-slate-500', textColor: 'text-slate-500 dark:text-slate-400', icon: Snowflake },
]; ];
return ( return (
<div className="bg-white rounded-xl border border-slate-200 p-5"> <div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5">
<h3 className="text-sm font-semibold text-slate-700 mb-4">Engagement Breakdown</h3> <h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-4">Engagement Breakdown</h3>
{/* Stacked bar */} {/* Stacked bar */}
<div className="flex rounded-full h-4 overflow-hidden mb-4"> <div className="flex rounded-full h-4 overflow-hidden mb-4">
{segments.map(s => ( {segments.map(s => (
@@ -111,12 +111,12 @@ function EngagementRing({ summary }: { summary: EngagementData['summary'] }) {
<div className={cn('w-3 h-3 rounded-full', s.color)} /> <div className={cn('w-3 h-3 rounded-full', s.color)} />
<div> <div>
<span className={cn('text-sm font-semibold', s.textColor)}>{s.count}</span> <span className={cn('text-sm font-semibold', s.textColor)}>{s.count}</span>
<span className="text-xs text-slate-400 ml-1">{s.label}</span> <span className="text-xs text-slate-400 dark:text-slate-500 ml-1">{s.label}</span>
</div> </div>
</div> </div>
))} ))}
</div> </div>
<p className="text-xs text-slate-400 mt-3"> <p className="text-xs text-slate-400 dark:text-slate-500 mt-3">
Engaged = contacted in last 14 days Warm = 15-30 days Cooling = 31-60 days Cold = 60+ days or never Engaged = contacted in last 14 days Warm = 15-30 days Cooling = 31-60 days Cold = 60+ days or never
</p> </p>
</div> </div>
@@ -130,22 +130,22 @@ function TopList({ title, items, icon: Icon }: {
}) { }) {
const max = Math.max(...items.map(i => i.value), 1); const max = Math.max(...items.map(i => i.value), 1);
return ( return (
<div className="bg-white rounded-xl border border-slate-200 p-5"> <div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Icon className="w-4 h-4 text-slate-400" /> <Icon className="w-4 h-4 text-slate-400 dark:text-slate-500" />
<h3 className="text-sm font-semibold text-slate-700">{title}</h3> <h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">{title}</h3>
</div> </div>
{items.length === 0 && ( {items.length === 0 && (
<p className="text-sm text-slate-400">No data yet</p> <p className="text-sm text-slate-400 dark:text-slate-500">No data yet</p>
)} )}
<div className="space-y-2.5"> <div className="space-y-2.5">
{items.slice(0, 8).map((item, i) => ( {items.slice(0, 8).map((item, i) => (
<div key={i}> <div key={i}>
<div className="flex justify-between text-sm mb-1"> <div className="flex justify-between text-sm mb-1">
<span className="text-slate-700 truncate">{item.label}</span> <span className="text-slate-700 dark:text-slate-300 truncate">{item.label}</span>
<span className="text-slate-500 font-medium ml-2">{item.value}</span> <span className="text-slate-500 dark:text-slate-400 font-medium ml-2">{item.value}</span>
</div> </div>
<div className="h-1.5 bg-slate-100 rounded-full overflow-hidden"> <div className="h-1.5 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
<div <div
className="h-full bg-blue-500 rounded-full transition-all" className="h-full bg-blue-500 rounded-full transition-all"
style={{ width: `${(item.value / max) * 100}%` }} style={{ width: `${(item.value / max) * 100}%` }}
@@ -164,23 +164,23 @@ function AtRiskList({ title, clients: clientList }: {
}) { }) {
if (clientList.length === 0) return null; if (clientList.length === 0) return null;
return ( return (
<div className="bg-white rounded-xl border border-slate-200 p-5"> <div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<AlertTriangle className="w-4 h-4 text-amber-500" /> <AlertTriangle className="w-4 h-4 text-amber-500" />
<h3 className="text-sm font-semibold text-slate-700">{title}</h3> <h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">{title}</h3>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{clientList.map(c => ( {clientList.map(c => (
<Link <Link
key={c.id} key={c.id}
to={`/clients/${c.id}`} to={`/clients/${c.id}`}
className="flex items-center justify-between p-2 rounded-lg hover:bg-slate-50 transition-colors" className="flex items-center justify-between p-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
> >
<div> <div>
<p className="text-sm font-medium text-slate-800">{c.name}</p> <p className="text-sm font-medium text-slate-800 dark:text-slate-200">{c.name}</p>
{c.company && <p className="text-xs text-slate-400">{c.company}</p>} {c.company && <p className="text-xs text-slate-400 dark:text-slate-500">{c.company}</p>}
</div> </div>
<span className="text-xs text-slate-400"> <span className="text-xs text-slate-400 dark:text-slate-500">
{c.lastContacted {c.lastContacted
? `${Math.floor((Date.now() - new Date(c.lastContacted).getTime()) / (1000 * 60 * 60 * 24))}d ago` ? `${Math.floor((Date.now() - new Date(c.lastContacted).getTime()) / (1000 * 60 * 60 * 24))}d ago`
: 'Never'} : 'Never'}
@@ -247,13 +247,13 @@ export default function ReportsPage() {
<div className="max-w-6xl mx-auto space-y-6 animate-fade-in"> <div className="max-w-6xl mx-auto space-y-6 animate-fade-in">
<div className="flex items-center justify-between flex-wrap gap-3"> <div className="flex items-center justify-between flex-wrap gap-3">
<div> <div>
<h1 className="text-2xl font-bold text-slate-900">Reports & Analytics</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Reports & Analytics</h1>
<p className="text-sm text-slate-500">Overview of your CRM performance</p> <p className="text-sm text-slate-500 dark:text-slate-400">Overview of your CRM performance</p>
</div> </div>
<button <button
onClick={handleExport} onClick={handleExport}
disabled={exporting} disabled={exporting}
className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors disabled:opacity-50" className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors disabled:opacity-50"
> >
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
{exporting ? 'Exporting…' : 'Export Clients CSV'} {exporting ? 'Exporting…' : 'Export Clients CSV'}
@@ -265,16 +265,16 @@ export default function ReportsPage() {
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard icon={Users} label="Total Clients" value={overview.clients.total} <StatCard icon={Users} label="Total Clients" value={overview.clients.total}
sub={`+${overview.clients.newThisMonth} this month`} sub={`+${overview.clients.newThisMonth} this month`}
color="bg-blue-50 text-blue-600" /> color="bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" />
<StatCard icon={Mail} label="Emails Sent" value={overview.emails.sent} <StatCard icon={Mail} label="Emails Sent" value={overview.emails.sent}
sub={`${overview.emails.sentLast30Days} last 30 days`} sub={`${overview.emails.sentLast30Days} last 30 days`}
color="bg-emerald-50 text-emerald-600" /> color="bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400" />
<StatCard icon={Calendar} label="Upcoming Events" value={overview.events.upcoming30Days} <StatCard icon={Calendar} label="Upcoming Events" value={overview.events.upcoming30Days}
sub="Next 30 days" sub="Next 30 days"
color="bg-amber-50 text-amber-600" /> color="bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400" />
<StatCard icon={TrendingUp} label="Contacted Recently" value={overview.clients.contactedRecently} <StatCard icon={TrendingUp} label="Contacted Recently" value={overview.clients.contactedRecently}
sub={`${overview.clients.neverContacted} never contacted`} sub={`${overview.clients.neverContacted} never contacted`}
color="bg-purple-50 text-purple-600" /> color="bg-purple-50 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400" />
</div> </div>
)} )}

View File

@@ -58,7 +58,7 @@ export default function ResetPasswordPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-slate-50"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-800">
<LoadingSpinner size="lg" /> <LoadingSpinner size="lg" />
</div> </div>
); );
@@ -66,16 +66,16 @@ export default function ResetPasswordPage() {
if (error) { if (error) {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-slate-50 p-4"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-800 p-4">
<div className="w-full max-w-md text-center"> <div className="w-full max-w-md text-center">
<div className="inline-flex items-center justify-center w-14 h-14 bg-red-100 rounded-2xl mb-4"> <div className="inline-flex items-center justify-center w-14 h-14 bg-red-100 dark:bg-red-900/50 rounded-2xl mb-4">
<Network className="w-8 h-8 text-red-500" /> <Network className="w-8 h-8 text-red-500 dark:text-red-400" />
</div> </div>
<h1 className="text-2xl font-bold text-slate-900 mb-2">Invalid Reset Link</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">Invalid Reset Link</h1>
<p className="text-slate-500 mb-6">{error}</p> <p className="text-slate-500 dark:text-slate-400 mb-6">{error}</p>
<Link <Link
to="/login" to="/login"
className="text-sm text-blue-600 hover:text-blue-700 font-medium" className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
> >
Back to login Back to login
</Link> </Link>
@@ -86,13 +86,13 @@ export default function ResetPasswordPage() {
if (success) { if (success) {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-slate-50 p-4"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-800 p-4">
<div className="w-full max-w-md text-center"> <div className="w-full max-w-md text-center">
<div className="inline-flex items-center justify-center w-14 h-14 bg-green-100 rounded-2xl mb-4"> <div className="inline-flex items-center justify-center w-14 h-14 bg-green-100 dark:bg-green-900/50 rounded-2xl mb-4">
<CheckCircle className="w-8 h-8 text-green-600" /> <CheckCircle className="w-8 h-8 text-green-600 dark:text-green-400" />
</div> </div>
<h1 className="text-2xl font-bold text-slate-900 mb-2">Password Reset</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">Password Reset</h1>
<p className="text-slate-500 mb-6">Your password has been successfully reset. You can now sign in with your new password.</p> <p className="text-slate-500 dark:text-slate-400 mb-6">Your password has been successfully reset. You can now sign in with your new password.</p>
<button <button
onClick={() => navigate('/login')} onClick={() => navigate('/login')}
className="px-6 py-2.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors" className="px-6 py-2.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors"
@@ -105,22 +105,22 @@ export default function ResetPasswordPage() {
} }
return ( return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-slate-50 flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-800 flex items-center justify-center p-4">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
{/* Logo */} {/* Logo */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-14 h-14 bg-blue-600 rounded-2xl mb-4"> <div className="inline-flex items-center justify-center w-14 h-14 bg-blue-600 rounded-2xl mb-4">
<Network className="w-8 h-8 text-white" /> <Network className="w-8 h-8 text-white" />
</div> </div>
<h1 className="text-2xl font-bold text-slate-900">Reset Password</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Reset Password</h1>
<p className="text-slate-500 mt-1">Choose a new password for your account</p> <p className="text-slate-500 dark:text-slate-400 mt-1">Choose a new password for your account</p>
</div> </div>
{/* Card */} {/* Card */}
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-8"> <div className="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-8">
{email && ( {email && (
<div className="mb-6 p-3 bg-blue-50 rounded-lg"> <div className="mb-6 p-3 bg-blue-50 dark:bg-blue-900/30 rounded-lg">
<p className="text-sm text-blue-700"> <p className="text-sm text-blue-700 dark:text-blue-300">
Resetting password for <strong>{email}</strong> Resetting password for <strong>{email}</strong>
</p> </p>
</div> </div>
@@ -128,13 +128,13 @@ export default function ResetPasswordPage() {
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} className="space-y-5">
{submitError && ( {submitError && (
<div className="p-3 bg-red-50 border border-red-200 text-red-700 text-sm rounded-lg"> <div className="p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 text-sm rounded-lg">
{submitError} {submitError}
</div> </div>
)} )}
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">New Password</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">New Password</label>
<div className="relative"> <div className="relative">
<input <input
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
@@ -142,13 +142,13 @@ export default function ResetPasswordPage() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
minLength={8} minLength={8}
className="w-full px-3.5 py-2.5 pr-10 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow" className="w-full px-3.5 py-2.5 pr-10 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
placeholder="Min 8 characters" placeholder="Min 8 characters"
/> />
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600" className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300"
> >
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />} {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button> </button>
@@ -156,13 +156,13 @@ export default function ResetPasswordPage() {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Confirm Password</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Confirm Password</label>
<input <input
type="password" type="password"
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
required required
className="w-full px-3.5 py-2.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow" className="w-full px-3.5 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
placeholder="Re-enter password" placeholder="Re-enter password"
/> />
</div> </div>
@@ -180,7 +180,7 @@ export default function ResetPasswordPage() {
<div className="mt-4 text-center"> <div className="mt-4 text-center">
<Link <Link
to="/login" to="/login"
className="text-sm text-slate-500 hover:text-slate-700" className="text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300"
> >
Back to login Back to login
</Link> </Link>

View File

@@ -7,7 +7,7 @@ import LoadingSpinner, { PageLoader } from '@/components/LoadingSpinner';
function StatusMessage({ type, message }: { type: 'success' | 'error'; message: string }) { function StatusMessage({ type, message }: { type: 'success' | 'error'; message: string }) {
return ( return (
<div className={`flex items-center gap-2 text-sm font-medium animate-fade-in ${type === 'success' ? 'text-emerald-600' : 'text-red-600'}`}> <div className={`flex items-center gap-2 text-sm font-medium animate-fade-in ${type === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'}`}>
{type === 'success' ? <CheckCircle2 className="w-4 h-4" /> : <AlertCircle className="w-4 h-4" />} {type === 'success' ? <CheckCircle2 className="w-4 h-4" /> : <AlertCircle className="w-4 h-4" />}
{message} {message}
</div> </div>
@@ -104,26 +104,26 @@ export default function SettingsPage() {
if (loading) return <PageLoader />; if (loading) return <PageLoader />;
const inputClass = 'w-full px-3.5 py-2.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent'; const inputClass = 'w-full px-3.5 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-slate-100 bg-white dark:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent';
const labelClass = 'block text-sm font-medium text-slate-700 mb-1.5'; const labelClass = 'block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5';
return ( return (
<div className="max-w-2xl mx-auto space-y-6 animate-fade-in"> <div className="max-w-2xl mx-auto space-y-6 animate-fade-in">
<div> <div>
<h1 className="text-2xl font-bold text-slate-900">Settings</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Settings</h1>
<p className="text-slate-500 text-sm mt-1">Manage your profile, email, and password</p> <p className="text-slate-500 dark:text-slate-400 text-sm mt-1">Manage your profile, email, and password</p>
</div> </div>
{/* Profile Information */} {/* Profile Information */}
<form onSubmit={handleSaveProfile} className="space-y-6"> <form onSubmit={handleSaveProfile} className="space-y-6">
<div className="bg-white border border-slate-200 rounded-xl p-6 space-y-5"> <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-6 space-y-5">
<div className="flex items-center gap-3 pb-4 border-b border-slate-100"> <div className="flex items-center gap-3 pb-4 border-b border-slate-100 dark:border-slate-700">
<div className="w-10 h-10 bg-blue-100 text-blue-700 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded-lg flex items-center justify-center">
<User className="w-5 h-5" /> <User className="w-5 h-5" />
</div> </div>
<div> <div>
<h2 className="font-semibold text-slate-900">Profile Information</h2> <h2 className="font-semibold text-slate-900 dark:text-slate-100">Profile Information</h2>
<p className="text-xs text-slate-500">Your public-facing details</p> <p className="text-xs text-slate-500 dark:text-slate-400">Your public-facing details</p>
</div> </div>
</div> </div>
@@ -194,14 +194,14 @@ export default function SettingsPage() {
{/* Change Email */} {/* Change Email */}
<form onSubmit={handleChangeEmail}> <form onSubmit={handleChangeEmail}>
<div className="bg-white border border-slate-200 rounded-xl p-6 space-y-5"> <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-6 space-y-5">
<div className="flex items-center gap-3 pb-4 border-b border-slate-100"> <div className="flex items-center gap-3 pb-4 border-b border-slate-100 dark:border-slate-700">
<div className="w-10 h-10 bg-amber-100 text-amber-700 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300 rounded-lg flex items-center justify-center">
<Mail className="w-5 h-5" /> <Mail className="w-5 h-5" />
</div> </div>
<div> <div>
<h2 className="font-semibold text-slate-900">Change Email</h2> <h2 className="font-semibold text-slate-900 dark:text-slate-100">Change Email</h2>
<p className="text-xs text-slate-500">Update your login email address</p> <p className="text-xs text-slate-500 dark:text-slate-400">Update your login email address</p>
</div> </div>
</div> </div>
@@ -233,14 +233,14 @@ export default function SettingsPage() {
{/* Change Password */} {/* Change Password */}
<form onSubmit={handleChangePassword}> <form onSubmit={handleChangePassword}>
<div className="bg-white border border-slate-200 rounded-xl p-6 space-y-5"> <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-6 space-y-5">
<div className="flex items-center gap-3 pb-4 border-b border-slate-100"> <div className="flex items-center gap-3 pb-4 border-b border-slate-100 dark:border-slate-700">
<div className="w-10 h-10 bg-emerald-100 text-emerald-700 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-emerald-100 dark:bg-emerald-900/50 text-emerald-700 dark:text-emerald-300 rounded-lg flex items-center justify-center">
<Lock className="w-5 h-5" /> <Lock className="w-5 h-5" />
</div> </div>
<div> <div>
<h2 className="font-semibold text-slate-900">Change Password</h2> <h2 className="font-semibold text-slate-900 dark:text-slate-100">Change Password</h2>
<p className="text-xs text-slate-500">Update your account password</p> <p className="text-xs text-slate-500 dark:text-slate-400">Update your account password</p>
</div> </div>
</div> </div>