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 { useAuthStore } from '@/stores/auth';
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';
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 }) {
const { isAuthenticated, isLoading } = useAuthStore();
if (isLoading) return <PageLoader />;
@@ -33,6 +34,7 @@ export default function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/login" element={
isAuthenticated ? <Navigate to="/" replace /> : <LoginPage />
@@ -56,6 +58,7 @@ export default function App() {
<Route path="admin" element={<AdminPage />} />
</Route>
</Routes>
</Suspense>
</BrowserRouter>
);
}

View File

@@ -1,13 +1,13 @@
import { cn } from '@/lib/utils';
const colorMap: Record<string, string> = {
blue: 'bg-blue-50 text-blue-700 border-blue-200',
green: 'bg-emerald-50 text-emerald-700 border-emerald-200',
yellow: 'bg-amber-50 text-amber-700 border-amber-200',
red: 'bg-red-50 text-red-700 border-red-200',
purple: 'bg-purple-50 text-purple-700 border-purple-200',
gray: 'bg-slate-50 text-slate-600 border-slate-200',
pink: 'bg-pink-50 text-pink-700 border-pink-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 dark:bg-emerald-900/50 dark:text-emerald-300 dark:border-emerald-800',
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 dark:bg-red-900/50 dark:text-red-300 dark:border-red-800',
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 dark:bg-slate-700 dark:text-slate-300 dark:border-slate-600',
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 {
@@ -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',
colorMap[color] || colorMap.gray,
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,
)}
>

View File

@@ -125,13 +125,13 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
<div className="space-y-4">
<div
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" />
<p className="text-sm font-medium text-slate-700">
<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 dark:text-slate-300">
Click to select a CSV file
</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
</p>
</div>
@@ -145,11 +145,11 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
{loading && (
<div className="flex items-center justify-center gap-2 py-4">
<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>
)}
{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" />
{error}
</div>
@@ -160,25 +160,25 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
{/* Step: Column Mapping */}
{step === 'mapping' && preview && (
<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" />
<span><strong>{preview.totalRows}</strong> rows found in <strong>{file?.name}</strong></span>
</div>
{/* Column mapping */}
<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">
{preview.headers.map((header, index) => (
<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}
</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
value={mapping[index] || ''}
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) => (
<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.sampleRows.length > 0 && (
<div>
<h4 className="text-sm font-medium text-slate-700 mb-2">Preview (first {preview.sampleRows.length} rows):</h4>
<div className="overflow-x-auto border border-slate-200 rounded-lg">
<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 dark:border-slate-700 rounded-lg">
<table className="w-full text-xs">
<thead>
<tr className="bg-slate-50">
<tr className="bg-slate-50 dark:bg-slate-700">
{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] ? (
<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>
))}
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
<tbody className="divide-y divide-slate-100 dark:divide-slate-700">
{preview.sampleRows.map((row, ri) => (
<tr key={ri}>
{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 || '—'}
</td>
))}
@@ -225,21 +225,21 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
)}
{!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" />
You must map both "First Name" and "Last Name" columns
</div>
)}
{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" />
{error}
</div>
)}
<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
</button>
<button
@@ -257,19 +257,19 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
{step === 'importing' && (
<div className="flex flex-col items-center justify-center py-8 gap-4">
<LoadingSpinner size="lg" />
<p className="text-sm text-slate-600">Importing clients...</p>
<p className="text-xs text-slate-400">This may take a moment for large files</p>
<p className="text-sm text-slate-600 dark:text-slate-300">Importing clients...</p>
<p className="text-xs text-slate-400 dark:text-slate-500">This may take a moment for large files</p>
</div>
)}
{/* Step: Results */}
{step === 'results' && result && (
<div className="space-y-4">
<div className="flex items-center gap-3 p-4 bg-emerald-50 rounded-lg">
<CheckCircle2 className="w-8 h-8 text-emerald-600 flex-shrink-0" />
<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 dark:text-emerald-400 flex-shrink-0" />
<div>
<p className="font-semibold text-emerald-800">Import Complete</p>
<p className="text-sm text-emerald-700">
<p className="font-semibold text-emerald-800 dark:text-emerald-300">Import Complete</p>
<p className="text-sm text-emerald-700 dark:text-emerald-400">
Successfully imported <strong>{result.imported}</strong> client{result.imported !== 1 ? 's' : ''}
{result.skipped > 0 && `, ${result.skipped} skipped`}
</p>
@@ -278,10 +278,10 @@ export default function CSVImportModal({ isOpen, onClose, onComplete }: CSVImpor
{result.errors.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-slate-700">Issues ({result.errors.length}):</h4>
<div className="max-h-40 overflow-y-auto space-y-1 p-3 bg-slate-50 rounded-lg">
<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 dark:bg-slate-700 rounded-lg">
{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>

View File

@@ -55,7 +55,6 @@ export default function ClientForm({ initialData, onSubmit, loading }: ClientFor
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Clean empty strings
const cleaned = Object.fromEntries(
Object.entries(form).filter(([_, v]) => v !== '' && v !== undefined && !(Array.isArray(v) && v.length === 0))
) as ClientCreate;
@@ -65,8 +64,8 @@ export default function ClientForm({ initialData, onSubmit, loading }: ClientFor
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 labelClass = 'block text-sm font-medium text-slate-700 mb-1';
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 dark:text-slate-300 mb-1';
return (
<form onSubmit={handleSubmit} className="space-y-5">
@@ -153,15 +152,15 @@ export default function ClientForm({ initialData, onSubmit, loading }: ClientFor
placeholder="Add tag..."
className={inputClass}
/>
<button type="button" onClick={addTag} className="px-3 py-2 bg-slate-100 hover:bg-slate-200 rounded-lg transition-colors">
<Plus className="w-4 h-4 text-slate-600" />
<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 dark:text-slate-300" />
</button>
</div>
<div className="flex flex-wrap gap-1.5">
{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}
<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>
))}
</div>
@@ -178,15 +177,15 @@ export default function ClientForm({ initialData, onSubmit, loading }: ClientFor
placeholder="Add interest..."
className={inputClass}
/>
<button type="button" onClick={addInterest} className="px-3 py-2 bg-slate-100 hover:bg-slate-200 rounded-lg transition-colors">
<Plus className="w-4 h-4 text-slate-600" />
<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 dark:text-slate-300" />
</button>
</div>
<div className="flex flex-wrap gap-1.5">
{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}
<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>
))}
</div>

View File

@@ -77,27 +77,29 @@ export default function EmailComposeModal({ isOpen, onClose, clientId, clientNam
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 (
<Modal isOpen={isOpen} onClose={() => { onClose(); reset(); }} title={`Email for ${clientName}`} size="lg">
{!generated ? (
<div className="space-y-5">
<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
value={purpose}
onChange={(e) => setPurpose(e.target.value)}
rows={3}
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>
<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
value={provider}
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="openai">OpenAI (GPT)</option>
@@ -116,7 +118,7 @@ export default function EmailComposeModal({ isOpen, onClose, clientId, clientNam
<button
onClick={handleGenerateBirthday}
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" />
Birthday
@@ -126,30 +128,30 @@ export default function EmailComposeModal({ isOpen, onClose, clientId, clientNam
) : (
<div className="space-y-4">
<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
value={editSubject}
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>
<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
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
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 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
</button>
<button
onClick={handleSave}
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'}
</button>

View File

@@ -13,11 +13,11 @@ interface EmptyStateProps {
export default function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
return (
<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">
<Icon className="w-8 h-8 text-slate-400" />
<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 dark:text-slate-500" />
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-1">{title}</h3>
<p className="text-sm text-slate-500 max-w-sm mb-6">{description}</p>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-1">{title}</h3>
<p className="text-sm text-slate-500 dark:text-slate-400 max-w-sm mb-6">{description}</p>
{action && (
<button
onClick={action.onClick}

View File

@@ -31,16 +31,16 @@ const typeIcons: Record<string, typeof Calendar> = {
};
const typeColors: Record<string, string> = {
overdue: 'text-red-500 bg-red-50',
upcoming: 'text-blue-500 bg-blue-50',
stale: 'text-amber-500 bg-amber-50',
drafts: 'text-purple-500 bg-purple-50',
overdue: 'text-red-500 bg-red-50 dark:bg-red-900/30',
upcoming: 'text-blue-500 bg-blue-50 dark:bg-blue-900/30',
stale: 'text-amber-500 bg-amber-50 dark:bg-amber-900/30',
drafts: 'text-purple-500 bg-purple-50 dark:bg-purple-900/30',
};
const priorityDot: Record<string, string> = {
high: 'bg-red-500',
medium: 'bg-amber-400',
low: 'bg-slate-300',
low: 'bg-slate-300 dark:bg-slate-500',
};
export default function NotificationBell() {
@@ -52,12 +52,10 @@ export default function NotificationBell() {
useEffect(() => {
fetchNotifications();
// Refresh every 5 minutes
const interval = setInterval(fetchNotifications, 5 * 60 * 1000);
return () => clearInterval(interval);
}, []);
// Close on outside click
useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
@@ -92,7 +90,7 @@ export default function NotificationBell() {
onClick={() => setOpen(!open)}
className={cn(
'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" />
@@ -107,13 +105,13 @@ export default function NotificationBell() {
</button>
{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="flex items-center justify-between px-4 py-3 border-b border-slate-100">
<h3 className="text-sm font-semibold text-slate-800">Notifications</h3>
<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 dark:border-slate-700">
<h3 className="text-sm font-semibold text-slate-800 dark:text-slate-200">Notifications</h3>
{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 && (
<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 && (
<span>{counts.upcoming} upcoming</span>
@@ -124,7 +122,7 @@ export default function NotificationBell() {
<div className="max-h-[400px] overflow-y-auto">
{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" />
<p className="text-sm">All caught up!</p>
</div>
@@ -132,20 +130,20 @@ export default function NotificationBell() {
visibleNotifs.map(n => {
const Icon = typeIcons[n.type] || Calendar;
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 className={cn('p-1.5 rounded-lg mt-0.5', typeColors[n.type] || 'bg-slate-50 text-slate-500')}>
<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 dark:bg-slate-700 text-slate-500')}>
<Icon className="w-4 h-4" />
</div>
<Link to={n.link} onClick={() => setOpen(false)} className="flex-1 min-w-0">
<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])} />
<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>
<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>
<button
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" />
</button>
@@ -156,11 +154,11 @@ export default function NotificationBell() {
</div>
{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
to="/reports"
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
</Link>

View File

@@ -137,24 +137,24 @@ export default function AdminPage() {
if (currentUser?.role !== 'admin') {
return (
<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>
);
}
return (
<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 */}
<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
onClick={() => setActiveTab('users')}
className={cn(
'pb-3 px-1 text-sm font-medium border-b-2 transition-colors',
activeTab === 'users'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-slate-500 hover:text-slate-700'
? 'border-blue-600 text-blue-600 dark:text-blue-400 dark:border-blue-400'
: '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" />
@@ -165,8 +165,8 @@ export default function AdminPage() {
className={cn(
'pb-3 px-1 text-sm font-medium border-b-2 transition-colors',
activeTab === 'invites'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-slate-500 hover:text-slate-700'
? 'border-blue-600 text-blue-600 dark:text-blue-400 dark:border-blue-400'
: '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" />
@@ -175,12 +175,12 @@ export default function AdminPage() {
</div>
{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' ? (
<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]">
<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">Email</th>
<th className="px-4 py-3 font-medium">Role</th>
@@ -193,23 +193,23 @@ export default function AdminPage() {
<tr
key={user.id}
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 text-slate-600">{user.email}</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 dark:text-slate-300">{user.email}</td>
<td className="px-4 py-3">
<span className={cn(
'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}
</span>
</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() : '—'}
</td>
<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>
</tr>
))}
@@ -228,29 +228,29 @@ export default function AdminPage() {
Invite User
</button>
) : (
<div className="mb-6 p-4 bg-slate-50 rounded-xl border border-slate-200">
<h3 className="font-medium text-slate-900 mb-4">Invite New User</h3>
<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 dark:text-slate-100 mb-4">Invite New User</h3>
{inviteUrl ? (
<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">
<input
type="text"
value={inviteUrl}
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
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>
</div>
<button
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
</button>
@@ -258,37 +258,37 @@ export default function AdminPage() {
) : (
<form onSubmit={handleCreateInvite} className="space-y-4">
{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>
<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
type="text"
value={inviteName}
onChange={(e) => setInviteName(e.target.value)}
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"
/>
</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
type="email"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
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"
/>
</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
value={inviteRole}
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="admin">Admin</option>
@@ -305,7 +305,7 @@ export default function AdminPage() {
<button
type="button"
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
</button>
@@ -316,10 +316,10 @@ export default function AdminPage() {
)}
{/* 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]">
<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">Email</th>
<th className="px-4 py-3 font-medium">Role</th>
@@ -331,19 +331,19 @@ export default function AdminPage() {
<tbody>
{invites.length === 0 ? (
<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
</td>
</tr>
) : (
invites.map((invite) => (
<tr key={invite.id} className="border-b border-slate-100 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 text-slate-600">{invite.email}</td>
<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 dark:text-slate-100">{invite.name}</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">
<span className={cn(
'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}
</span>
@@ -351,21 +351,21 @@ export default function AdminPage() {
<td className="px-4 py-3">
<span className={cn(
'px-2 py-1 text-xs rounded-full',
invite.status === 'accepted' ? 'bg-green-100 text-green-700' :
invite.status === 'expired' ? 'bg-red-100 text-red-700' :
'bg-yellow-100 text-yellow-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 dark:bg-red-900/50 text-red-700 dark:text-red-300' :
'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300'
)}>
{invite.status}
</span>
</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()}
</td>
<td className="px-4 py-3">
{invite.status === 'pending' && (
<button
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" />
</button>
@@ -385,17 +385,17 @@ export default function AdminPage() {
<>
{/* Backdrop */}
<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}
/>
{/* 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 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-200">
<h2 className="text-lg font-semibold text-slate-900">User Settings</h2>
<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 dark:text-slate-100">User Settings</h2>
<button
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" />
</button>
@@ -405,33 +405,33 @@ export default function AdminPage() {
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{/* User info */}
<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() || '?'}
</div>
<div>
<h3 className="font-medium text-slate-900">{selectedUser.name}</h3>
<p className="text-sm text-slate-500">{selectedUser.email}</p>
<h3 className="font-medium text-slate-900 dark:text-slate-100">{selectedUser.name}</h3>
<p className="text-sm text-slate-500 dark:text-slate-400">{selectedUser.email}</p>
</div>
</div>
{/* Details */}
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Joined</label>
<p className="text-sm text-slate-600">
<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 dark:text-slate-300">
{selectedUser.createdAt ? new Date(selectedUser.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : '—'}
</p>
</div>
{/* Role */}
<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 ? (
<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}
</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 className="flex gap-2">
@@ -440,8 +440,8 @@ export default function AdminPage() {
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium border transition-colors',
selectedUser.role === 'user'
? 'bg-slate-100 border-slate-300 text-slate-900'
: 'border-slate-200 text-slate-500 hover:bg-slate-50'
? 'bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100'
: '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" />
@@ -452,8 +452,8 @@ export default function AdminPage() {
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium border transition-colors',
selectedUser.role === 'admin'
? 'bg-purple-100 border-purple-300 text-purple-700'
: 'border-slate-200 text-slate-500 hover:bg-slate-50'
? '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 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" />
@@ -466,24 +466,24 @@ export default function AdminPage() {
{/* Actions — only for non-self users */}
{selectedUser.id !== currentUser?.id && (
<div className="space-y-4 pt-4 border-t border-slate-200">
<h4 className="text-sm font-medium text-slate-700">Actions</h4>
<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 dark:text-slate-300">Actions</h4>
{/* 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">
<KeyRound className="w-4 h-4 text-slate-500" />
<span className="text-sm font-medium text-slate-700">Password Reset</span>
<KeyRound className="w-4 h-4 text-slate-500 dark:text-slate-400" />
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">Password Reset</span>
</div>
{resetUrl ? (
<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">
<input
type="text"
value={resetUrl}
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
onClick={handleCopyResetUrl}
@@ -498,7 +498,7 @@ export default function AdminPage() {
<button
onClick={() => handleGenerateResetLink(selectedUser.id)}
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'}
</button>
@@ -506,12 +506,12 @@ export default function AdminPage() {
</div>
{/* 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">
<Trash2 className="w-4 h-4 text-red-500" />
<span className="text-sm font-medium text-red-700">Danger Zone</span>
<Trash2 className="w-4 h-4 text-red-500 dark:text-red-400" />
<span className="text-sm font-medium text-red-700 dark:text-red-300">Danger Zone</span>
</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
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"

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="flex items-center justify-between flex-wrap gap-3">
<div>
<h1 className="text-2xl font-bold text-slate-900">Emails</h1>
<p className="text-slate-500 text-sm mt-1">AI-generated emails for your network</p>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Emails</h1>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">AI-generated emails for your network</p>
</div>
<button
onClick={() => setShowCompose(true)}
@@ -87,8 +87,8 @@ export default function EmailsPage() {
className={cn(
'px-3 py-1.5 rounded-full text-sm font-medium transition-colors',
statusFilter === key
? 'bg-blue-100 text-blue-700'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300'
: 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600'
)}
>
{label}
@@ -107,22 +107,22 @@ export default function EmailsPage() {
) : (
<div className="space-y-3">
{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 ? (
<div className="space-y-3">
<input
value={editSubject}
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
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
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">
<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>
</div>
</div>
@@ -133,28 +133,28 @@ export default function EmailsPage() {
<div className="flex items-center gap-2 mb-1">
<EmailStatusBadge status={email.status} />
{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}
</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>
<h3 className="font-semibold text-slate-900 mb-2">{email.subject}</h3>
<p className="text-sm text-slate-600 whitespace-pre-wrap line-clamp-4">{email.content}</p>
<h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-2">{email.subject}</h3>
<p className="text-sm text-slate-600 dark:text-slate-300 whitespace-pre-wrap line-clamp-4">{email.content}</p>
</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' && (
<>
<button
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
</button>
<button
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
</button>
@@ -162,7 +162,7 @@ export default function EmailsPage() {
)}
<button
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
</button>
@@ -178,11 +178,11 @@ export default function EmailsPage() {
<Modal isOpen={showCompose} onClose={() => setShowCompose(false)} title="Generate Email" size="md">
<div className="space-y-4">
<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={composeForm.clientId}
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>
{clients.map((c) => (
@@ -191,21 +191,21 @@ export default function EmailsPage() {
</select>
</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
value={composeForm.purpose}
onChange={(e) => setComposeForm({ ...composeForm, purpose: e.target.value })}
rows={3}
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>
<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
value={composeForm.provider}
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="openai">OpenAI (GPT)</option>
@@ -223,7 +223,7 @@ export default function EmailsPage() {
<button
onClick={handleGenerateBirthday}
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" />
Birthday

View File

@@ -1,14 +1,14 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useMemo } from 'react';
import { useEventsStore } from '@/stores/events';
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 Badge, { EventTypeBadge } from '@/components/Badge';
import EmptyState from '@/components/EmptyState';
import { PageLoader } from '@/components/LoadingSpinner';
import Modal from '@/components/Modal';
import LoadingSpinner from '@/components/LoadingSpinner';
import type { EventCreate } from '@/types';
import type { EventCreate, Event } from '@/types';
import { Link } from 'react-router-dom';
const eventTypes = [
@@ -19,11 +19,179 @@ const eventTypes = [
{ 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() {
const { events, isLoading, typeFilter, setTypeFilter, fetchEvents, createEvent, deleteEvent, syncAll } = useEventsStore();
const { clients, fetchClients } = useClientsStore();
const [showCreate, setShowCreate] = 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(() => {
fetchEvents();
@@ -53,14 +221,41 @@ export default function EventsPage() {
<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>
<h1 className="text-2xl font-bold text-slate-900">Events</h1>
<p className="text-slate-500 text-sm mt-1">{events.length} events tracked</p>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Events</h1>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">{events.length} events tracked</p>
</div>
<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
onClick={handleSync}
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')} />
Sync All
@@ -84,8 +279,8 @@ export default function EventsPage() {
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium transition-colors',
typeFilter === key
? 'bg-blue-100 text-blue-700'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300'
: '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" />
@@ -94,7 +289,63 @@ export default function EventsPage() {
))}
</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 ? (
<EmptyState
icon={Calendar}
@@ -103,27 +354,27 @@ export default function EventsPage() {
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) => {
const days = getDaysUntil(event.date);
return (
<div key={event.id} className="flex items-center gap-4 px-5 py-4">
<div className={cn(
'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 <= 7 ? 'bg-amber-50 text-amber-600' :
'bg-slate-50 text-slate-600'
days <= 1 ? 'bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400' :
days <= 7 ? 'bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400' :
'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-[10px] uppercase">{new Date(event.date).toLocaleString('en', { month: 'short' })}</span>
</div>
<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">
<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 && (
<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}
</Link>
)}
@@ -132,17 +383,17 @@ export default function EventsPage() {
<div className="text-right">
<p className={cn(
'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`}
</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>
<button
onClick={async () => {
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>
@@ -151,6 +402,8 @@ export default function EventsPage() {
})}
</div>
)}
</>
)}
{/* Create Modal */}
<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 (
<Modal isOpen={isOpen} onClose={onClose} title="Create Event">
<form onSubmit={handleSubmit} className="space-y-4">
<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} />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<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}>
<option value="custom">Custom</option>
<option value="birthday">Birthday</option>
@@ -215,12 +468,12 @@ function CreateEventModal({ isOpen, onClose, clients, onCreate }: {
</select>
</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} />
</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}>
<option value="">None</option>
{clients.map((c) => (
@@ -229,22 +482,22 @@ function CreateEventModal({ isOpen, onClose, clients, onCreate }: {
</select>
</div>
<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
type="checkbox"
checked={form.recurring}
onChange={(e) => setForm({ ...form, recurring: e.target.checked })}
className="rounded border-slate-300"
className="rounded border-slate-300 dark:border-slate-600"
/>
Recurring annually
</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>
<input
type="number"
value={form.reminderDays || ''}
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}
/>
<span>days before</span>

View File

@@ -26,15 +26,15 @@ export default function ForgotPasswordPage() {
};
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">
{/* Logo */}
<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">
<Network className="w-8 h-8 text-white" />
</div>
<h1 className="text-2xl font-bold text-slate-900">Forgot Password</h1>
<p className="text-slate-500 mt-1">
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Forgot Password</h1>
<p className="text-slate-500 dark:text-slate-400 mt-1">
{submitted
? 'Check your email or contact your admin'
: 'Enter your email to request a password reset'}
@@ -42,17 +42,17 @@ export default function ForgotPasswordPage() {
</div>
{/* 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 ? (
<div className="text-center">
<div className="p-4 bg-green-50 border border-green-200 rounded-lg mb-6">
<p className="text-sm text-green-700">
<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 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.
</p>
</div>
<Link
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" />
Back to login
@@ -61,19 +61,19 @@ export default function ForgotPasswordPage() {
) : (
<form onSubmit={handleSubmit} className="space-y-5">
{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}
</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
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
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"
/>
</div>
@@ -90,7 +90,7 @@ export default function ForgotPasswordPage() {
<div className="text-center">
<Link
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" />
Back to login

View File

@@ -51,11 +51,9 @@ export default function InvitePage() {
setSubmitting(true);
try {
const result = await api.acceptInvite(token!, password, name);
// If we got a token, store it
if (result.token) {
api.setToken(result.token);
}
// Now log in with the credentials
try {
await api.login(invite!.email, password);
} catch {
@@ -72,7 +70,7 @@ export default function InvitePage() {
if (loading) {
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" />
</div>
);
@@ -80,58 +78,58 @@ export default function InvitePage() {
if (error) {
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="inline-flex items-center justify-center w-14 h-14 bg-red-100 rounded-2xl mb-4">
<Network className="w-8 h-8 text-red-500" />
<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 dark:text-red-400" />
</div>
<h1 className="text-2xl font-bold text-slate-900 mb-2">Invalid Invite</h1>
<p className="text-slate-500">{error}</p>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">Invalid Invite</h1>
<p className="text-slate-500 dark:text-slate-400">{error}</p>
</div>
</div>
);
}
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">
{/* Logo */}
<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">
<Network className="w-8 h-8 text-white" />
</div>
<h1 className="text-2xl font-bold text-slate-900">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>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Welcome to NetworkCRM</h1>
<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>
{/* Card */}
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-8">
<div className="mb-6 p-3 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-700">
<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 dark:bg-blue-900/30 rounded-lg">
<p className="text-sm text-blue-700 dark:text-blue-300">
Setting up account for <strong>{invite?.email}</strong>
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{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}
</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
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
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>
<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">
<input
type={showPassword ? 'text' : 'password'}
@@ -139,13 +137,13 @@ export default function InvitePage() {
onChange={(e) => setPassword(e.target.value)}
required
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"
/>
<button
type="button"
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" />}
</button>
@@ -153,13 +151,13 @@ export default function InvitePage() {
</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
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
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"
/>
</div>

View File

@@ -9,19 +9,19 @@ import {
import { cn } from '@/lib/utils';
import { PageLoader } from '@/components/LoadingSpinner';
const categoryConfig: Record<string, { label: string; icon: typeof Network; color: string; bg: string }> = {
industry: { label: 'Industry', icon: Building2, color: 'text-blue-600', bg: 'bg-blue-50' },
interests: { label: 'Interests', icon: Heart, color: 'text-pink-600', bg: 'bg-pink-50' },
location: { label: 'Location', icon: MapPin, color: 'text-emerald-600', bg: 'bg-emerald-50' },
business: { label: 'Business', icon: Briefcase, color: 'text-purple-600', bg: 'bg-purple-50' },
social: { label: 'Social', icon: Users, color: 'text-amber-600', bg: 'bg-amber-50' },
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', darkColor: 'dark:text-blue-400', bg: 'bg-blue-50', darkBg: 'dark:bg-blue-900/30' },
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', darkColor: 'dark:text-emerald-400', bg: 'bg-emerald-50', darkBg: 'dark:bg-emerald-900/30' },
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', darkColor: 'dark:text-amber-400', bg: 'bg-amber-50', darkBg: 'dark:bg-amber-900/30' },
};
function ScoreBadge({ score }: { score: number }) {
const color = score >= 60 ? 'bg-emerald-100 text-emerald-700' :
score >= 40 ? 'bg-blue-100 text-blue-700' :
score >= 25 ? 'bg-amber-100 text-amber-700' :
'bg-slate-100 text-slate-600';
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 dark:bg-blue-900/50 dark:text-blue-300' :
score >= 25 ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300' :
'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300';
return (
<span className={cn('px-2 py-0.5 rounded-full text-xs font-bold', color)}>
{score}%
@@ -38,10 +38,10 @@ function MatchCard({ match, onGenerateIntro, generatingIntro }: {
const Icon = config.icon;
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 */}
<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" />
{config.label}
</div>
@@ -52,40 +52,40 @@ function MatchCard({ match, onGenerateIntro, generatingIntro }: {
<div className="flex items-center gap-3 mb-3">
<Link
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('')}
</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>
<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>
<Link
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('')}
</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>
</div>
{/* Reasons */}
<div className="space-y-1.5 mb-3">
{match.reasons.map((reason, i) => (
<div key={i} className="flex items-start gap-2 text-sm text-slate-600">
<span className="text-emerald-500 mt-0.5 flex-shrink-0"></span>
<div key={i} className="flex items-start gap-2 text-sm text-slate-600 dark:text-slate-300">
<span className="text-emerald-500 dark:text-emerald-400 mt-0.5 flex-shrink-0"></span>
{reason}
</div>
))}
</div>
{/* Intro suggestion */}
<div className="bg-slate-50 rounded-lg p-3 mb-3">
<p className="text-sm text-slate-600 italic">{match.introSuggestion}</p>
<div className="bg-slate-50 dark:bg-slate-700 rounded-lg p-3 mb-3">
<p className="text-sm text-slate-600 dark:text-slate-300 italic">{match.introSuggestion}</p>
</div>
{/* Actions */}
@@ -93,7 +93,7 @@ function MatchCard({ match, onGenerateIntro, generatingIntro }: {
<button
onClick={() => onGenerateIntro(match)}
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 ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
@@ -165,8 +165,8 @@ export default function NetworkPage() {
{/* Header */}
<div className="flex items-center justify-between flex-wrap gap-3">
<div>
<h1 className="text-2xl font-bold text-slate-900">Network Matching</h1>
<p className="text-slate-500 text-sm mt-1">
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Network Matching</h1>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">
AI-powered suggestions for connecting your clients
</p>
</div>
@@ -175,33 +175,33 @@ export default function NetworkPage() {
{/* Stats */}
{stats && (
<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">
<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>
<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 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">
<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>
<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 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">
<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>
<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 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">
<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>
<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 || '—'}
</p>
</div>
@@ -210,20 +210,20 @@ export default function NetworkPage() {
{/* Top Connectors */}
{stats && stats.topConnectors.length > 0 && (
<div className="bg-white border border-slate-200 rounded-xl p-5">
<h3 className="font-semibold text-slate-900 mb-3">Most Connected Clients</h3>
<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 dark:text-slate-100 mb-3">Most Connected Clients</h3>
<div className="flex flex-wrap gap-3">
{stats.topConnectors.map((c) => (
<Link
key={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('')}
</div>
<span className="text-sm font-medium text-slate-700">{c.name}</span>
<span className="text-xs text-slate-400">{c.matchCount} matches</span>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">{c.name}</span>
<span className="text-xs text-slate-400 dark:text-slate-500">{c.matchCount} matches</span>
</Link>
))}
</div>
@@ -232,7 +232,7 @@ export default function NetworkPage() {
{/* Filters */}
<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:
</div>
@@ -240,7 +240,7 @@ export default function NetworkPage() {
onClick={() => setCategoryFilter(null)}
className={cn(
'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})
@@ -255,7 +255,7 @@ export default function NetworkPage() {
onClick={() => setCategoryFilter(categoryFilter === cat ? null : cat)}
className={cn(
'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})
@@ -263,11 +263,11 @@ export default function NetworkPage() {
);
})}
<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
value={minScore}
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={20}>20%</option>
@@ -279,10 +279,10 @@ export default function NetworkPage() {
{/* Match Cards */}
{filtered.length === 0 ? (
<div className="bg-white border border-slate-200 rounded-xl p-12 text-center">
<Network className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<h3 className="font-semibold text-slate-900 mb-1">No matches found</h3>
<p className="text-sm text-slate-500 mb-4">
<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 dark:text-slate-600 mx-auto mb-3" />
<h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">No matches found</h3>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-4">
{matches.length === 0
? 'Add more clients with detailed profiles (interests, industry, location) to find connections.'
: '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;
}) {
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)}>
<Icon className="w-5 h-5" />
</div>
<div>
<p className="text-2xl font-bold text-slate-900">{value}</p>
<p className="text-sm text-slate-500">{label}</p>
{sub && <p className="text-xs text-slate-400 mt-0.5">{sub}</p>}
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{value}</p>
<p className="text-sm text-slate-500 dark:text-slate-400">{label}</p>
{sub && <p className="text-xs text-slate-400 dark:text-slate-500 mt-0.5">{sub}</p>}
</div>
</div>
);
@@ -59,17 +59,17 @@ function BarChartSimple({ data, label, color }: {
}) {
const max = Math.max(...data.map(d => d.value), 1);
return (
<div className="bg-white rounded-xl border border-slate-200 p-5">
<h3 className="text-sm font-semibold text-slate-700 mb-4">{label}</h3>
<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 dark:text-slate-300 mb-4">{label}</h3>
<div className="flex items-end gap-1.5 h-32">
{data.map((d, i) => (
<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
className={cn('w-full rounded-t transition-all', color)}
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}
</span>
</div>
@@ -83,15 +83,15 @@ function EngagementRing({ summary }: { summary: EngagementData['summary'] }) {
const total = summary.engaged + summary.warm + summary.cooling + summary.cold;
if (total === 0) return null;
const segments = [
{ label: 'Engaged', count: summary.engaged, color: 'bg-emerald-500', textColor: 'text-emerald-600', icon: Flame },
{ label: 'Warm', count: summary.warm, color: 'bg-amber-400', textColor: 'text-amber-600', icon: ThermometerSun },
{ label: 'Cooling', count: summary.cooling, color: 'bg-blue-400', textColor: 'text-blue-600', icon: Activity },
{ label: 'Cold', count: summary.cold, color: 'bg-slate-300', textColor: 'text-slate-500', icon: Snowflake },
{ 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 dark:text-amber-400', icon: ThermometerSun },
{ 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 dark:bg-slate-500', textColor: 'text-slate-500 dark:text-slate-400', icon: Snowflake },
];
return (
<div className="bg-white rounded-xl border border-slate-200 p-5">
<h3 className="text-sm font-semibold text-slate-700 mb-4">Engagement Breakdown</h3>
<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 dark:text-slate-300 mb-4">Engagement Breakdown</h3>
{/* Stacked bar */}
<div className="flex rounded-full h-4 overflow-hidden mb-4">
{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>
<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>
<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
</p>
</div>
@@ -130,22 +130,22 @@ function TopList({ title, items, icon: Icon }: {
}) {
const max = Math.max(...items.map(i => i.value), 1);
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">
<Icon className="w-4 h-4 text-slate-400" />
<h3 className="text-sm font-semibold text-slate-700">{title}</h3>
<Icon className="w-4 h-4 text-slate-400 dark:text-slate-500" />
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">{title}</h3>
</div>
{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">
{items.slice(0, 8).map((item, i) => (
<div key={i}>
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-700 truncate">{item.label}</span>
<span className="text-slate-500 font-medium ml-2">{item.value}</span>
<span className="text-slate-700 dark:text-slate-300 truncate">{item.label}</span>
<span className="text-slate-500 dark:text-slate-400 font-medium ml-2">{item.value}</span>
</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
className="h-full bg-blue-500 rounded-full transition-all"
style={{ width: `${(item.value / max) * 100}%` }}
@@ -164,23 +164,23 @@ function AtRiskList({ title, clients: clientList }: {
}) {
if (clientList.length === 0) return null;
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">
<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 className="space-y-2">
{clientList.map(c => (
<Link
key={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>
<p className="text-sm font-medium text-slate-800">{c.name}</p>
{c.company && <p className="text-xs text-slate-400">{c.company}</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 dark:text-slate-500">{c.company}</p>}
</div>
<span className="text-xs text-slate-400">
<span className="text-xs text-slate-400 dark:text-slate-500">
{c.lastContacted
? `${Math.floor((Date.now() - new Date(c.lastContacted).getTime()) / (1000 * 60 * 60 * 24))}d ago`
: 'Never'}
@@ -247,13 +247,13 @@ export default function ReportsPage() {
<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>
<h1 className="text-2xl font-bold text-slate-900">Reports & Analytics</h1>
<p className="text-sm text-slate-500">Overview of your CRM performance</p>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Reports & Analytics</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">Overview of your CRM performance</p>
</div>
<button
onClick={handleExport}
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" />
{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">
<StatCard icon={Users} label="Total Clients" value={overview.clients.total}
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}
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}
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}
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>
)}

View File

@@ -58,7 +58,7 @@ export default function ResetPasswordPage() {
if (loading) {
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" />
</div>
);
@@ -66,16 +66,16 @@ export default function ResetPasswordPage() {
if (error) {
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="inline-flex items-center justify-center w-14 h-14 bg-red-100 rounded-2xl mb-4">
<Network className="w-8 h-8 text-red-500" />
<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 dark:text-red-400" />
</div>
<h1 className="text-2xl font-bold text-slate-900 mb-2">Invalid Reset Link</h1>
<p className="text-slate-500 mb-6">{error}</p>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">Invalid Reset Link</h1>
<p className="text-slate-500 dark:text-slate-400 mb-6">{error}</p>
<Link
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
</Link>
@@ -86,13 +86,13 @@ export default function ResetPasswordPage() {
if (success) {
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="inline-flex items-center justify-center w-14 h-14 bg-green-100 rounded-2xl mb-4">
<CheckCircle className="w-8 h-8 text-green-600" />
<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 dark:text-green-400" />
</div>
<h1 className="text-2xl font-bold text-slate-900 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>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">Password Reset</h1>
<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
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"
@@ -105,22 +105,22 @@ export default function ResetPasswordPage() {
}
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">
{/* Logo */}
<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">
<Network className="w-8 h-8 text-white" />
</div>
<h1 className="text-2xl font-bold text-slate-900">Reset Password</h1>
<p className="text-slate-500 mt-1">Choose a new password for your account</p>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Reset Password</h1>
<p className="text-slate-500 dark:text-slate-400 mt-1">Choose a new password for your account</p>
</div>
{/* 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 && (
<div className="mb-6 p-3 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-700">
<div className="mb-6 p-3 bg-blue-50 dark:bg-blue-900/30 rounded-lg">
<p className="text-sm text-blue-700 dark:text-blue-300">
Resetting password for <strong>{email}</strong>
</p>
</div>
@@ -128,13 +128,13 @@ export default function ResetPasswordPage() {
<form onSubmit={handleSubmit} className="space-y-5">
{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}
</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">
<input
type={showPassword ? 'text' : 'password'}
@@ -142,13 +142,13 @@ export default function ResetPasswordPage() {
onChange={(e) => setPassword(e.target.value)}
required
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"
/>
<button
type="button"
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" />}
</button>
@@ -156,13 +156,13 @@ export default function ResetPasswordPage() {
</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
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
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"
/>
</div>
@@ -180,7 +180,7 @@ export default function ResetPasswordPage() {
<div className="mt-4 text-center">
<Link
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
</Link>

View File

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