fix: wizard grey screen, missing form fields, and implement settings page
- Fix Tailwind v4 modal overlay: bg-opacity-75 removed, use bg-gray-500/75 - Restructure AddMailAccountModal with flexbox + relative z-10 - Add missing required fields to form: forward_to, delivery_method, delete_after_forward; auto-sync email_address from username - Fix DashboardLayout mobile overlay opacity (same Tailwind v4 issue) - Make email_address/forward_to required in MailAccountCreate interface - Add userApi.updateProfile() method - Implement Settings page: profile, account info, security sections - Update CHANGELOG.md and docs/TODO.md Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/d3a9e3bc-5637-45c5-b674-8ef436953614
This commit is contained in:
@@ -1,19 +1,216 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AuthGuard } from '@/components/AuthGuard';
|
||||
import { DashboardLayout } from '@/components/DashboardLayout';
|
||||
import { userApi } from '@/lib/api';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { CheckCircle, Loader2, User, Mail, Shield } from 'lucide-react';
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<DashboardLayout>
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<p className="text-gray-600">Settings page coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsContent />
|
||||
</DashboardLayout>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsContent() {
|
||||
const queryClient = useQueryClient();
|
||||
const { user, setUser } = useAuthStore();
|
||||
|
||||
const [profileForm, setProfileForm] = useState({
|
||||
full_name: user?.full_name || '',
|
||||
email: user?.email || '',
|
||||
});
|
||||
const [profileSaved, setProfileSaved] = useState(false);
|
||||
|
||||
// Refresh user data from the server
|
||||
const { data: currentUser } = useQuery({
|
||||
queryKey: ['current-user'],
|
||||
queryFn: userApi.getCurrentUser,
|
||||
initialData: user ?? undefined,
|
||||
});
|
||||
|
||||
// Sync form when server data arrives
|
||||
useEffect(() => {
|
||||
if (currentUser) {
|
||||
setProfileForm({
|
||||
full_name: currentUser.full_name || '',
|
||||
email: currentUser.email || '',
|
||||
});
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
const updateProfileMutation = useMutation({
|
||||
mutationFn: (data: { full_name: string; email: string }) =>
|
||||
userApi.updateProfile(data),
|
||||
onSuccess: (updatedUser) => {
|
||||
setUser(updatedUser);
|
||||
queryClient.invalidateQueries({ queryKey: ['current-user'] });
|
||||
setProfileSaved(true);
|
||||
setTimeout(() => setProfileSaved(false), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const handleProfileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setProfileForm((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleProfileSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await updateProfileMutation.mutateAsync({
|
||||
full_name: profileForm.full_name,
|
||||
email: profileForm.email,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error && 'response' in error
|
||||
? (error as { response?: { data?: { detail?: string } } }).response?.data
|
||||
?.detail
|
||||
: null;
|
||||
alert(errorMessage || 'Failed to update profile');
|
||||
}
|
||||
};
|
||||
|
||||
const displayUser = currentUser ?? user;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
|
||||
|
||||
{/* Profile Section */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center gap-2">
|
||||
<User className="h-5 w-5 text-gray-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Profile</h2>
|
||||
</div>
|
||||
<form onSubmit={handleProfileSubmit} className="px-6 py-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="full_name"
|
||||
value={profileForm.full_name}
|
||||
onChange={handleProfileChange}
|
||||
className="w-full max-w-md px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Your full name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={profileForm.email}
|
||||
onChange={handleProfileChange}
|
||||
className="w-full max-w-md px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={updateProfileMutation.isPending}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{updateProfileMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Saving…
|
||||
</>
|
||||
) : (
|
||||
'Save Changes'
|
||||
)}
|
||||
</button>
|
||||
{profileSaved && (
|
||||
<span className="flex items-center text-sm text-green-600">
|
||||
<CheckCircle className="h-4 w-4 mr-1" />
|
||||
Saved successfully
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Account Information */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center gap-2">
|
||||
<Mail className="h-5 w-5 text-gray-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Account Information</h2>
|
||||
</div>
|
||||
<div className="px-6 py-6 space-y-3">
|
||||
<div className="flex items-center text-sm">
|
||||
<span className="text-gray-500 w-40">Subscription tier:</span>
|
||||
<span className="font-medium text-gray-900 capitalize">
|
||||
{displayUser?.subscription_tier ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm">
|
||||
<span className="text-gray-500 w-40">Subscription status:</span>
|
||||
<span className="font-medium text-gray-900 capitalize">
|
||||
{displayUser?.subscription_status ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm">
|
||||
<span className="text-gray-500 w-40">Member since:</span>
|
||||
<span className="text-gray-900">
|
||||
{displayUser?.created_at
|
||||
? new Date(displayUser.created_at).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
: '—'}
|
||||
</span>
|
||||
</div>
|
||||
{displayUser?.oauth_provider && (
|
||||
<div className="flex items-center text-sm">
|
||||
<span className="text-gray-500 w-40">Linked account:</span>
|
||||
<span className="font-medium text-gray-900 capitalize">
|
||||
{displayUser.oauth_provider}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{displayUser?.last_login_at && (
|
||||
<div className="flex items-center text-sm">
|
||||
<span className="text-gray-500 w-40">Last login:</span>
|
||||
<span className="text-gray-900">
|
||||
{new Date(displayUser.last_login_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security Section */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 text-gray-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Security</h2>
|
||||
</div>
|
||||
<div className="px-6 py-6">
|
||||
<p className="text-sm text-gray-600">
|
||||
Password change and two-factor authentication settings are coming soon.
|
||||
</p>
|
||||
{displayUser?.oauth_provider === 'google' && (
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Your account is authenticated via Google OAuth — password management is handled by Google.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { mailAccountsApi, MailAccount, MailAccountCreate } from '@/lib/api';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { X, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { ProviderWizard } from './ProviderWizard';
|
||||
|
||||
@@ -15,6 +16,7 @@ type WizardStep = 'provider' | 'form';
|
||||
|
||||
export function AddMailAccountModal({ account, onClose }: AddMailAccountModalProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { user } = useAuthStore();
|
||||
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
|
||||
const [testMessage, setTestMessage] = useState('');
|
||||
const [autoDetecting, setAutoDetecting] = useState(false);
|
||||
@@ -22,14 +24,20 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
|
||||
const [formData, setFormData] = useState<MailAccountCreate>({
|
||||
name: account?.name || '',
|
||||
protocol: account?.protocol || 'pop3',
|
||||
email_address: account?.email_address || '',
|
||||
protocol: account?.protocol || 'pop3_ssl',
|
||||
host: account?.host || '',
|
||||
port: account?.port || 995,
|
||||
username: '',
|
||||
password: '',
|
||||
use_ssl: account?.use_ssl ?? true,
|
||||
use_tls: account?.use_tls ?? false,
|
||||
forward_to: account?.forward_to || user?.email || '',
|
||||
delivery_method: account?.delivery_method || 'gmail_api',
|
||||
is_enabled: account?.is_enabled ?? true,
|
||||
check_interval_minutes: account?.check_interval_minutes || 5,
|
||||
max_emails_per_check: account?.max_emails_per_check || 100,
|
||||
max_emails_per_check: account?.max_emails_per_check || 50,
|
||||
delete_after_forward: account?.delete_after_forward ?? true,
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
@@ -43,11 +51,21 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked :
|
||||
type === 'number' ? Number(value) : value,
|
||||
}));
|
||||
const newValue =
|
||||
type === 'checkbox'
|
||||
? (e.target as HTMLInputElement).checked
|
||||
: type === 'number'
|
||||
? Number(value)
|
||||
: value;
|
||||
|
||||
setFormData((prev) => {
|
||||
const updated = { ...prev, [name]: newValue };
|
||||
// Keep email_address in sync with username unless explicitly changed
|
||||
if (name === 'username') {
|
||||
updated.email_address = value;
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const handleProviderSelect = (config: { name: string; protocol: string; host: string; port: number; use_ssl: boolean }) => {
|
||||
@@ -110,8 +128,8 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
setTestMessage('Connection successful!');
|
||||
} catch (error) {
|
||||
setTestStatus('error');
|
||||
const errorMessage = error instanceof Error && 'response' in error
|
||||
? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
|
||||
const errorMessage = error instanceof Error && 'response' in error
|
||||
? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
|
||||
: null;
|
||||
setTestMessage(errorMessage || 'Connection failed');
|
||||
}
|
||||
@@ -122,8 +140,8 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
try {
|
||||
await createMutation.mutateAsync(formData);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error && 'response' in error
|
||||
? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
|
||||
const errorMessage = error instanceof Error && 'response' in error
|
||||
? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
|
||||
: null;
|
||||
alert(errorMessage || 'Failed to save account');
|
||||
}
|
||||
@@ -131,10 +149,10 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-screen items-center justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose} />
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<div className="fixed inset-0 bg-gray-500/75 transition-opacity" onClick={onClose} />
|
||||
|
||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full">
|
||||
<div className="relative z-10 bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all w-full sm:my-8 sm:max-w-2xl">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="bg-white px-6 pt-6 pb-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
@@ -270,21 +288,69 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="use_ssl"
|
||||
id="use_ssl"
|
||||
checked={formData.use_ssl}
|
||||
onChange={handleChange}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="use_ssl" className="ml-2 block text-sm text-gray-700">
|
||||
Use SSL/TLS
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="use_ssl"
|
||||
id="use_ssl"
|
||||
checked={formData.use_ssl}
|
||||
onChange={handleChange}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="use_ssl" className="ml-2 block text-sm text-gray-700">
|
||||
Use SSL/TLS
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="delete_after_forward"
|
||||
id="delete_after_forward"
|
||||
checked={formData.delete_after_forward}
|
||||
onChange={handleChange}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="delete_after_forward" className="ml-2 block text-sm text-gray-700">
|
||||
Delete after forwarding
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Forward To (destination email)
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="forward_to"
|
||||
value={formData.forward_to}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="you@gmail.com"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Fetched emails will be delivered to this address.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Delivery Method
|
||||
</label>
|
||||
<select
|
||||
name="delivery_method"
|
||||
value={formData.delivery_method}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="gmail_api">Gmail API (recommended)</option>
|
||||
<option value="smtp">SMTP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Check Interval (minutes)
|
||||
@@ -296,31 +362,25 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro
|
||||
onChange={handleChange}
|
||||
required
|
||||
min="1"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Max Emails Per Check
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="max_emails_per_check"
|
||||
value={formData.max_emails_per_check}
|
||||
onChange={handleChange}
|
||||
min="1"
|
||||
max="1440"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-3">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Delivery:</strong> Emails will be delivered to your Gmail account.
|
||||
Configure your Gmail API credentials in Settings for direct injection (recommended),
|
||||
or they will be forwarded via SMTP.
|
||||
</p>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Max Emails Per Check
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="max_emails_per_check"
|
||||
value={formData.max_emails_per_check}
|
||||
onChange={handleChange}
|
||||
min="1"
|
||||
max="1000"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{testStatus !== 'idle' && (
|
||||
|
||||
@@ -77,7 +77,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
{/* Mobile sidebar */}
|
||||
{sidebarOpen && (
|
||||
<div className="fixed inset-0 z-40 lg:hidden">
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" onClick={() => setSidebarOpen(false)} />
|
||||
<div className="fixed inset-0 bg-gray-600/75" onClick={() => setSidebarOpen(false)} />
|
||||
<div className="fixed inset-y-0 left-0 flex w-64 flex-col bg-white">
|
||||
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-200">
|
||||
<h1 className="text-xl font-bold text-gray-900">POP3 Forwarder</h1>
|
||||
|
||||
@@ -79,7 +79,7 @@ export interface MailAccount {
|
||||
|
||||
export interface MailAccountCreate {
|
||||
name: string;
|
||||
email_address?: string;
|
||||
email_address: string;
|
||||
protocol: string;
|
||||
host: string;
|
||||
port: number;
|
||||
@@ -87,7 +87,7 @@ export interface MailAccountCreate {
|
||||
use_tls?: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
forward_to?: string;
|
||||
forward_to: string;
|
||||
delivery_method?: string;
|
||||
is_enabled?: boolean;
|
||||
check_interval_minutes?: number;
|
||||
@@ -176,6 +176,11 @@ export const userApi = {
|
||||
const response = await api.get<User>("/users/me");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateProfile(data: { full_name?: string; email?: string }): Promise<User> {
|
||||
const response = await api.put<User>("/users/me", data);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ── Mail Accounts API ───────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user