mirror of
https://github.com/root-fr/jmap-webmail.git
synced 2026-09-24 00:01:14 +00:00
fix: Improve UI/UX with dark mode selection visibility and auto-select inbox on login
- Fix dark mode folder selection not visible by improving accent color contrast (#1e3a8a) - Fix inbox not selected by default on login (auto-select primary account inbox) - Fix email store not cleared on logout (proper state reset between sessions) - Add settings page with appearance, email behavior, and advanced options - Integrate mark-as-read delay setting with proper useEffect implementation - Reset dataLoaded flag on logout to ensure fresh data load on next login 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
1e563a8973
commit
2f454cd673
@@ -118,7 +118,13 @@
|
||||
- [x] Add smooth email loading transitions with cross-fade effect
|
||||
- [x] Implement functional quick reply with auto-expand and direct send
|
||||
- [ ] Implement error boundaries
|
||||
- [ ] Create settings page
|
||||
- [x] Create settings page
|
||||
- [x] Integrate mark-as-read delay setting in email viewer
|
||||
- [ ] Integrate delete action setting (trash vs permanent)
|
||||
- [ ] Integrate show preview toggle in email list
|
||||
- [ ] Integrate external content policy with image loading
|
||||
- [ ] Integrate debug mode with console logging
|
||||
- [ ] Integrate animations toggle throughout components
|
||||
- [ ] Add keyboard shortcuts (j/k navigation, etc.)
|
||||
- [ ] Implement drag-and-drop for emails
|
||||
- [ ] Add context menus
|
||||
@@ -205,4 +211,7 @@
|
||||
- [x] Fixed light mode not applying properly (replaced hardcoded colors with CSS variables)
|
||||
- [x] Fixed all hardcoded colors in email viewer for proper theme support
|
||||
- [x] Fixed sidebar footer menu extending page height when scrolling (overflow containment)
|
||||
- [x] Fixed dark mode folder selection not visible (improved accent color contrast)
|
||||
- [x] Fixed inbox not selected by default on login (auto-select primary account inbox)
|
||||
- [x] Fixed email store not cleared on logout (proper state reset)
|
||||
|
||||
|
||||
+58
-15
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Sidebar } from "@/components/layout/sidebar";
|
||||
@@ -9,6 +9,7 @@ import { EmailViewer } from "@/components/email/email-viewer";
|
||||
import { EmailComposer } from "@/components/email/email-composer";
|
||||
import { useEmailStore } from "@/stores/email-store";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
@@ -18,6 +19,7 @@ export default function Home() {
|
||||
const [composerMode, setComposerMode] = useState<'compose' | 'reply' | 'replyAll' | 'forward'>('compose');
|
||||
const [dataLoaded, setDataLoaded] = useState(false);
|
||||
const [initialCheckDone, setInitialCheckDone] = useState(false);
|
||||
const markAsReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const { isAuthenticated, client, logout, checkAuth, isLoading: authLoading } = useAuthStore();
|
||||
const {
|
||||
emails,
|
||||
@@ -80,9 +82,10 @@ export default function Home() {
|
||||
});
|
||||
}, [checkAuth]);
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
// Redirect to login if not authenticated and reset data loaded flag
|
||||
useEffect(() => {
|
||||
if (initialCheckDone && !isAuthenticated && !authLoading) {
|
||||
setDataLoaded(false); // Reset so data is reloaded on next login
|
||||
router.push(`/${params.locale}/login`);
|
||||
}
|
||||
}, [initialCheckDone, isAuthenticated, authLoading, router, params.locale]);
|
||||
@@ -92,21 +95,19 @@ export default function Home() {
|
||||
if (isAuthenticated && client && !dataLoaded) {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// First fetch mailboxes and quota
|
||||
// First fetch mailboxes and quota (inbox will be auto-selected in fetchMailboxes)
|
||||
await Promise.all([
|
||||
fetchMailboxes(client),
|
||||
fetchQuota(client)
|
||||
]);
|
||||
|
||||
// Get the actual inbox mailbox ID
|
||||
// Get the selected mailbox (should be inbox by default)
|
||||
const state = useEmailStore.getState();
|
||||
const inboxMailbox = state.mailboxes.find(m => m.role === 'inbox');
|
||||
const selectedMailboxId = state.selectedMailbox;
|
||||
|
||||
if (inboxMailbox) {
|
||||
// Update selected mailbox to actual inbox ID
|
||||
state.selectMailbox(inboxMailbox.id);
|
||||
// Now fetch emails with correct mailbox ID
|
||||
await fetchEmails(client, inboxMailbox.id);
|
||||
// Fetch emails for the selected mailbox
|
||||
if (selectedMailboxId) {
|
||||
await fetchEmails(client, selectedMailboxId);
|
||||
} else {
|
||||
await fetchEmails(client);
|
||||
}
|
||||
@@ -120,6 +121,52 @@ export default function Home() {
|
||||
}
|
||||
}, [isAuthenticated, client, dataLoaded, fetchMailboxes, fetchEmails, fetchQuota]);
|
||||
|
||||
// Handle mark-as-read with delay based on settings
|
||||
useEffect(() => {
|
||||
// Clear any existing timeout when email changes
|
||||
if (markAsReadTimeoutRef.current) {
|
||||
console.log('[Mark as Read] Clearing previous timeout');
|
||||
clearTimeout(markAsReadTimeoutRef.current);
|
||||
markAsReadTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
// Only set timeout if there's a selected email, it's unread, and we have a client
|
||||
if (!selectedEmail || !client || selectedEmail.keywords?.$seen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current setting value
|
||||
const markAsReadDelay = useSettingsStore.getState().markAsReadDelay;
|
||||
console.log('[Mark as Read] Delay setting:', markAsReadDelay, 'ms for email:', selectedEmail.id);
|
||||
|
||||
if (markAsReadDelay === -1) {
|
||||
// Never mark as read automatically
|
||||
console.log('[Mark as Read] Never mode - email will stay unread');
|
||||
} else if (markAsReadDelay === 0) {
|
||||
// Mark as read instantly
|
||||
console.log('[Mark as Read] Instant mode - marking as read now');
|
||||
markAsRead(client, selectedEmail.id, true);
|
||||
} else {
|
||||
// Mark as read after delay
|
||||
console.log('[Mark as Read] Delayed mode - will mark as read in', markAsReadDelay, 'ms');
|
||||
markAsReadTimeoutRef.current = setTimeout(() => {
|
||||
console.log('[Mark as Read] Timeout fired - marking as read now');
|
||||
markAsRead(client, selectedEmail.id, true);
|
||||
markAsReadTimeoutRef.current = null;
|
||||
}, markAsReadDelay);
|
||||
}
|
||||
|
||||
// Cleanup on unmount or when dependencies change
|
||||
return () => {
|
||||
if (markAsReadTimeoutRef.current) {
|
||||
console.log('[Mark as Read] Cleanup - clearing timeout');
|
||||
clearTimeout(markAsReadTimeoutRef.current);
|
||||
markAsReadTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedEmail?.id]);
|
||||
|
||||
const handleEmailSend = async (data: {
|
||||
to: string[];
|
||||
cc: string[];
|
||||
@@ -332,11 +379,7 @@ export default function Home() {
|
||||
const fullEmail = await client.getEmail(email.id, accountId);
|
||||
if (fullEmail) {
|
||||
selectEmail(fullEmail);
|
||||
|
||||
// Automatically mark as read after opening (if unread)
|
||||
if (!fullEmail.keywords?.$seen) {
|
||||
await markAsRead(client, fullEmail.id, true);
|
||||
}
|
||||
// Mark-as-read logic is now handled by useEffect
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch email content:', error);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ArrowLeft, Settings as SettingsIcon } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AppearanceSettings } from '@/components/settings/appearance-settings';
|
||||
import { EmailSettings } from '@/components/settings/email-settings';
|
||||
import { AccountSettings } from '@/components/settings/account-settings';
|
||||
import { AdvancedSettings } from '@/components/settings/advanced-settings';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type Tab = 'appearance' | 'email' | 'account' | 'advanced';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const t = useTranslations('settings');
|
||||
const [activeTab, setActiveTab] = useState<Tab>('appearance');
|
||||
|
||||
const tabs: { id: Tab; label: string }[] = [
|
||||
{ id: 'appearance', label: t('tabs.appearance') },
|
||||
{ id: 'email', label: t('tabs.email') },
|
||||
{ id: 'account', label: t('tabs.account') },
|
||||
{ id: 'advanced', label: t('tabs.advanced') },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background">
|
||||
{/* Settings Sidebar */}
|
||||
<div className="w-64 border-r border-border bg-secondary flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-border">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/${params.locale}`)}
|
||||
className="w-full justify-start"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
{t('back_to_mail')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex-1 overflow-y-auto py-2">
|
||||
<div className="px-2 space-y-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={cn(
|
||||
'w-full text-left px-3 py-2 rounded text-sm transition-colors',
|
||||
activeTab === tab.id
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-muted text-foreground'
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="max-w-3xl mx-auto p-8">
|
||||
{/* Page Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<SettingsIcon className="w-8 h-8 text-foreground" />
|
||||
<h1 className="text-3xl font-semibold text-foreground">{t('title')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Tab Content */}
|
||||
<div className="bg-card border border-border rounded-lg p-6">
|
||||
{activeTab === 'appearance' && <AppearanceSettings />}
|
||||
{activeTab === 'email' && <EmailSettings />}
|
||||
{activeTab === 'account' && <AccountSettings />}
|
||||
{activeTab === 'advanced' && <AdvancedSettings />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+8
-2
@@ -16,6 +16,11 @@
|
||||
--color-muted-foreground: #64748b;
|
||||
--color-accent: #dbeafe;
|
||||
--color-accent-foreground: #1e40af;
|
||||
|
||||
/* Settings variables */
|
||||
--font-size-base: 16px;
|
||||
--list-item-height: 48px;
|
||||
--transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -30,8 +35,8 @@
|
||||
--color-secondary-foreground: #fafafa;
|
||||
--color-muted: #262626;
|
||||
--color-muted-foreground: #a3a3a3;
|
||||
--color-accent: #262626;
|
||||
--color-accent-foreground: #fafafa;
|
||||
--color-accent: #1e3a8a;
|
||||
--color-accent-foreground: #dbeafe;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -58,6 +63,7 @@ body {
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: var(--font-size-base);
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,8 +67,12 @@ export function EmailListItem({ email, selected, onClick }: EmailListItemProps)
|
||||
isChecked && "ring-2 ring-primary/20 bg-accent/40"
|
||||
)}
|
||||
onClick={onClick}
|
||||
style={{ minHeight: 'var(--list-item-height)' }}
|
||||
>
|
||||
<div className="flex items-start gap-3 px-4 py-4">
|
||||
<div className="flex items-start gap-3 px-4" style={{
|
||||
paddingTop: 'calc((var(--list-item-height) - 40px) / 2)',
|
||||
paddingBottom: 'calc((var(--list-item-height) - 40px) / 2)'
|
||||
}}>
|
||||
{/* Checkbox with smooth animation */}
|
||||
<button
|
||||
onClick={handleCheckboxClick}
|
||||
|
||||
@@ -6,8 +6,6 @@ import { useParams, useRouter, usePathname } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { LanguageSwitcher } from "@/components/ui/language-switcher";
|
||||
import { useThemeStore } from "@/stores/theme-store";
|
||||
import { locales } from "@/i18n/request";
|
||||
import {
|
||||
Inbox,
|
||||
Send,
|
||||
@@ -208,8 +206,6 @@ export function Sidebar({
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [menuView, setMenuView] = useState<'main' | 'settings'>('main');
|
||||
const { theme, setTheme, resolvedTheme } = useThemeStore();
|
||||
const t = useTranslations('sidebar');
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
@@ -372,9 +368,7 @@ export function Sidebar({
|
||||
"transform transition-all duration-300 ease-out",
|
||||
showMenu ? "-translate-y-12" : "translate-y-full"
|
||||
)}>
|
||||
{/* Main Menu View */}
|
||||
{menuView === 'main' && (
|
||||
<div className="py-2">
|
||||
<div className="py-2">
|
||||
{/* Storage Info */}
|
||||
{quota && quota.total > 0 && (
|
||||
<div className="px-4 py-2">
|
||||
@@ -396,7 +390,7 @@ export function Sidebar({
|
||||
<div className="border-t border-border mt-2 pt-2">
|
||||
{/* Settings */}
|
||||
<button
|
||||
onClick={() => setMenuView('settings')}
|
||||
onClick={() => router.push(`/${params.locale}/settings`)}
|
||||
className="w-full px-4 py-2 flex items-center justify-between hover:bg-muted transition-colors text-sm"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
@@ -417,95 +411,13 @@ export function Sidebar({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings Submenu View */}
|
||||
{menuView === 'settings' && (
|
||||
<div className="py-2">
|
||||
{/* Back Button */}
|
||||
<button
|
||||
onClick={() => setMenuView('main')}
|
||||
className="w-full px-4 py-2 flex items-center gap-2 hover:bg-muted transition-colors text-sm border-b border-border mb-2"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
<span className="font-medium">{t("settings")}</span>
|
||||
</button>
|
||||
|
||||
{/* Theme */}
|
||||
<div className="px-4 py-2 space-y-1">
|
||||
<div className="text-xs text-muted-foreground mb-2">Theme</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setTheme('light')}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-1.5 text-xs rounded transition-colors",
|
||||
theme === 'light'
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
Light
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme('dark')}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-1.5 text-xs rounded transition-colors",
|
||||
theme === 'dark'
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
Dark
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme('system')}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-1.5 text-xs rounded transition-colors",
|
||||
theme === 'system'
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
System
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Language */}
|
||||
<div className="px-4 py-2 space-y-1">
|
||||
<div className="text-xs text-muted-foreground mb-2">Language</div>
|
||||
<div className="flex gap-2">
|
||||
{locales.map((locale) => (
|
||||
<button
|
||||
key={locale}
|
||||
onClick={() => {
|
||||
const pathWithoutLocale = pathname.replace(`/${params.locale}`, '');
|
||||
router.push(`/${locale}${pathWithoutLocale}`);
|
||||
}}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-1.5 text-xs rounded transition-colors",
|
||||
params.locale === locale
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
{locale === 'en' ? 'English' : 'Français'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Menu Toggle Button */}
|
||||
<div className="border-t border-border relative">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowMenu(!showMenu);
|
||||
if (!showMenu) setMenuView('main');
|
||||
}}
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
className={cn(
|
||||
"w-full px-4 py-3 flex items-center justify-between",
|
||||
"hover:bg-muted transition-colors",
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useEmailStore } from '@/stores/email-store';
|
||||
import { SettingsSection, SettingItem } from './settings-section';
|
||||
import { formatFileSize } from '@/lib/utils';
|
||||
|
||||
export function AccountSettings() {
|
||||
const t = useTranslations('settings.account');
|
||||
const { username, serverUrl } = useAuthStore();
|
||||
const { quota } = useEmailStore();
|
||||
|
||||
const quotaPercentage = quota ? Math.round((quota.used / quota.total) * 100) : 0;
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{/* Email Address */}
|
||||
<SettingItem label={t('email.label')}>
|
||||
<span className="text-sm text-foreground">{username || t('../../common.unknown')}</span>
|
||||
</SettingItem>
|
||||
|
||||
{/* Server */}
|
||||
<SettingItem label={t('server.label')}>
|
||||
<span className="text-sm text-foreground truncate max-w-xs">
|
||||
{serverUrl || t('../../common.unknown')}
|
||||
</span>
|
||||
</SettingItem>
|
||||
|
||||
{/* Storage */}
|
||||
{quota && quota.total > 0 && (
|
||||
<SettingItem
|
||||
label={t('storage.label')}
|
||||
description={t('storage.used', {
|
||||
used: formatFileSize(quota.used),
|
||||
total: formatFileSize(quota.total),
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span className="text-sm text-foreground">
|
||||
{t('storage.percentage', { percent: quotaPercentage })}
|
||||
</span>
|
||||
<div className="w-32 h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary rounded-full transition-all"
|
||||
style={{ width: `${quotaPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingItem>
|
||||
)}
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export function AdvancedSettings() {
|
||||
const t = useTranslations('settings.advanced');
|
||||
const tCommon = useTranslations('common');
|
||||
const { debugMode, updateSetting, resetToDefaults, exportSettings, importSettings } =
|
||||
useSettingsStore();
|
||||
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleExport = () => {
|
||||
const settingsJson = exportSettings();
|
||||
const blob = new Blob([settingsJson], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `webmail-settings-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const json = event.target?.result as string;
|
||||
const success = importSettings(json);
|
||||
if (success) {
|
||||
alert(t('../../settings.import_success'));
|
||||
} else {
|
||||
alert(t('../../settings.import_error'));
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (showResetConfirm) {
|
||||
resetToDefaults();
|
||||
setShowResetConfirm(false);
|
||||
alert(t('../../settings.save_success'));
|
||||
} else {
|
||||
setShowResetConfirm(true);
|
||||
setTimeout(() => setShowResetConfirm(false), 5000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{/* Debug Mode */}
|
||||
<SettingItem label={t('debug_mode.label')} description={t('debug_mode.description')}>
|
||||
<ToggleSwitch checked={debugMode} onChange={(checked) => updateSetting('debugMode', checked)} />
|
||||
</SettingItem>
|
||||
|
||||
{/* Export Settings */}
|
||||
<SettingItem label={t('export_settings.label')} description={t('export_settings.description')}>
|
||||
<Button variant="outline" size="sm" onClick={handleExport}>
|
||||
{t('export_settings.button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
|
||||
{/* Import Settings */}
|
||||
<SettingItem label={t('import_settings.label')} description={t('import_settings.description')}>
|
||||
<>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button variant="outline" size="sm" onClick={handleImport}>
|
||||
{t('import_settings.button')}
|
||||
</Button>
|
||||
</>
|
||||
</SettingItem>
|
||||
|
||||
{/* Reset Settings */}
|
||||
<SettingItem label={t('reset_settings.label')} description={t('reset_settings.description')}>
|
||||
<Button
|
||||
variant={showResetConfirm ? 'destructive' : 'outline'}
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
>
|
||||
{showResetConfirm ? tCommon('yes') : t('reset_settings.button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useThemeStore } from '@/stores/theme-store';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section';
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const t = useTranslations('settings.appearance');
|
||||
const { theme, setTheme } = useThemeStore();
|
||||
const { fontSize, listDensity, animationsEnabled, updateSetting } = useSettingsStore();
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{/* Theme */}
|
||||
<SettingItem label={t('theme.label')} description={t('theme.description')}>
|
||||
<RadioGroup
|
||||
value={theme}
|
||||
onChange={(value) => setTheme(value as 'light' | 'dark' | 'system')}
|
||||
options={[
|
||||
{ value: 'light', label: t('theme.light') },
|
||||
{ value: 'dark', label: t('theme.dark') },
|
||||
{ value: 'system', label: t('theme.system') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Font Size */}
|
||||
<SettingItem label={t('font_size.label')} description={t('font_size.description')}>
|
||||
<RadioGroup
|
||||
value={fontSize}
|
||||
onChange={(value) => updateSetting('fontSize', value as 'small' | 'medium' | 'large')}
|
||||
options={[
|
||||
{ value: 'small', label: t('font_size.small') },
|
||||
{ value: 'medium', label: t('font_size.medium') },
|
||||
{ value: 'large', label: t('font_size.large') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* List Density */}
|
||||
<SettingItem label={t('list_density.label')} description={t('list_density.description')}>
|
||||
<RadioGroup
|
||||
value={listDensity}
|
||||
onChange={(value) =>
|
||||
updateSetting('listDensity', value as 'compact' | 'regular' | 'comfortable')
|
||||
}
|
||||
options={[
|
||||
{ value: 'compact', label: t('list_density.compact') },
|
||||
{ value: 'regular', label: t('list_density.regular') },
|
||||
{ value: 'comfortable', label: t('list_density.comfortable') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Animations */}
|
||||
<SettingItem label={t('animations.label')} description={t('animations.description')}>
|
||||
<ToggleSwitch
|
||||
checked={animationsEnabled}
|
||||
onChange={(checked) => updateSetting('animationsEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
|
||||
|
||||
export function EmailSettings() {
|
||||
const t = useTranslations('settings.email_behavior');
|
||||
const {
|
||||
markAsReadDelay,
|
||||
deleteAction,
|
||||
showPreview,
|
||||
emailsPerPage,
|
||||
externalContentPolicy,
|
||||
updateSetting,
|
||||
} = useSettingsStore();
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{/* Mark as Read */}
|
||||
<SettingItem label={t('mark_read.label')} description={t('mark_read.description')}>
|
||||
<Select
|
||||
value={markAsReadDelay.toString()}
|
||||
onChange={(value) => updateSetting('markAsReadDelay', parseInt(value))}
|
||||
options={[
|
||||
{ value: '0', label: t('mark_read.instant') },
|
||||
{ value: '3000', label: t('mark_read.delay_3s') },
|
||||
{ value: '5000', label: t('mark_read.delay_5s') },
|
||||
{ value: '-1', label: t('mark_read.never') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Delete Action */}
|
||||
<SettingItem label={t('delete_action.label')} description={t('delete_action.description')}>
|
||||
<Select
|
||||
value={deleteAction}
|
||||
onChange={(value) => updateSetting('deleteAction', value as 'trash' | 'permanent')}
|
||||
options={[
|
||||
{ value: 'trash', label: t('delete_action.trash') },
|
||||
{ value: 'permanent', label: t('delete_action.permanent') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Show Preview */}
|
||||
<SettingItem label={t('show_preview.label')} description={t('show_preview.description')}>
|
||||
<ToggleSwitch checked={showPreview} onChange={(checked) => updateSetting('showPreview', checked)} />
|
||||
</SettingItem>
|
||||
|
||||
{/* Emails Per Page */}
|
||||
<SettingItem label={t('emails_per_page.label')} description={t('emails_per_page.description')}>
|
||||
<Select
|
||||
value={emailsPerPage.toString()}
|
||||
onChange={(value) => updateSetting('emailsPerPage', parseInt(value))}
|
||||
options={[
|
||||
{ value: '25', label: t('emails_per_page.25') },
|
||||
{ value: '50', label: t('emails_per_page.50') },
|
||||
{ value: '100', label: t('emails_per_page.100') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* External Content */}
|
||||
<SettingItem label={t('external_content.label')} description={t('external_content.description')}>
|
||||
<Select
|
||||
value={externalContentPolicy}
|
||||
onChange={(value) =>
|
||||
updateSetting('externalContentPolicy', value as 'ask' | 'block' | 'allow')
|
||||
}
|
||||
options={[
|
||||
{ value: 'ask', label: t('external_content.ask') },
|
||||
{ value: 'block', label: t('external_content.block') },
|
||||
{ value: 'allow', label: t('external_content.allow') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface SettingsSectionProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function SettingsSection({ title, description, children }: SettingsSectionProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground">{title}</h3>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SettingItemProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function SettingItem({ label, description, children }: SettingItemProps) {
|
||||
return (
|
||||
<div className="flex items-start justify-between py-3 border-b border-border last:border-0">
|
||||
<div className="flex-1 pr-4">
|
||||
<label className="text-sm font-medium text-foreground">{label}</label>
|
||||
{description && (
|
||||
<p className="text-xs text-muted-foreground mt-1">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToggleSwitchProps {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ToggleSwitch({ checked, onChange, disabled }: ToggleSwitchProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 items-center rounded-full transition-colors
|
||||
${checked ? 'bg-primary' : 'bg-muted'}
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-block h-4 w-4 transform rounded-full bg-background transition-transform
|
||||
${checked ? 'translate-x-6' : 'translate-x-1'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface RadioGroupProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options: { value: string; label: string }[];
|
||||
}
|
||||
|
||||
export function RadioGroup({ value, onChange, options }: RadioGroupProps) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => onChange(option.value)}
|
||||
className={`
|
||||
px-3 py-1.5 text-xs rounded transition-colors
|
||||
${
|
||||
value === option.value
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-accent text-foreground'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options: { value: string; label: string }[];
|
||||
}
|
||||
|
||||
export function Select({ value, onChange, options }: SelectProps) {
|
||||
return (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="px-3 py-1.5 text-sm rounded bg-muted border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
@@ -199,5 +199,210 @@
|
||||
"title": "Language",
|
||||
"english": "English",
|
||||
"french": "Français"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"back_to_mail": "Back to Mail",
|
||||
"save_success": "Settings saved successfully",
|
||||
"import_success": "Settings imported successfully",
|
||||
"import_error": "Failed to import settings",
|
||||
"reset_confirm": "Are you sure you want to reset all settings to defaults?",
|
||||
"tabs": {
|
||||
"appearance": "Appearance",
|
||||
"language": "Language & Region",
|
||||
"email": "Email Behavior",
|
||||
"composer": "Composer",
|
||||
"privacy": "Privacy & Security",
|
||||
"account": "Account",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"description": "Customize the look and feel of your webmail",
|
||||
"theme": {
|
||||
"label": "Theme",
|
||||
"description": "Choose your preferred color scheme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"font_size": {
|
||||
"label": "Font Size",
|
||||
"description": "Adjust text size for better readability",
|
||||
"small": "Small",
|
||||
"medium": "Medium",
|
||||
"large": "Large"
|
||||
},
|
||||
"list_density": {
|
||||
"label": "List Density",
|
||||
"description": "Control spacing in email lists",
|
||||
"compact": "Compact",
|
||||
"regular": "Regular",
|
||||
"comfortable": "Comfortable"
|
||||
},
|
||||
"animations": {
|
||||
"label": "Enable Animations",
|
||||
"description": "Show smooth transitions and effects"
|
||||
}
|
||||
},
|
||||
"language_region": {
|
||||
"title": "Language & Region",
|
||||
"description": "Configure language and regional preferences",
|
||||
"language": {
|
||||
"label": "Language",
|
||||
"description": "Choose your preferred language",
|
||||
"english": "English",
|
||||
"french": "Français"
|
||||
},
|
||||
"date_format": {
|
||||
"label": "Date Format",
|
||||
"description": "How dates should be displayed",
|
||||
"regional": "Regional",
|
||||
"iso": "ISO 8601",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"time_format": {
|
||||
"label": "Time Format",
|
||||
"description": "Choose between 12-hour or 24-hour clock",
|
||||
"12h": "12-hour",
|
||||
"24h": "24-hour"
|
||||
},
|
||||
"first_day": {
|
||||
"label": "First Day of Week",
|
||||
"description": "Start week on Sunday or Monday",
|
||||
"sunday": "Sunday",
|
||||
"monday": "Monday"
|
||||
}
|
||||
},
|
||||
"email_behavior": {
|
||||
"title": "Email Behavior",
|
||||
"description": "Configure how emails are handled",
|
||||
"mark_read": {
|
||||
"label": "Mark as Read",
|
||||
"description": "When to mark emails as read when opened",
|
||||
"instant": "Instantly",
|
||||
"delay_3s": "After 3 seconds",
|
||||
"delay_5s": "After 5 seconds",
|
||||
"never": "Never"
|
||||
},
|
||||
"delete_action": {
|
||||
"label": "Delete Action",
|
||||
"description": "What happens when you delete an email",
|
||||
"trash": "Move to Trash",
|
||||
"permanent": "Delete Permanently"
|
||||
},
|
||||
"show_preview": {
|
||||
"label": "Show Preview Text",
|
||||
"description": "Display email preview in the list"
|
||||
},
|
||||
"emails_per_page": {
|
||||
"label": "Emails Per Page",
|
||||
"description": "Number of emails to load at once",
|
||||
"25": "25 emails",
|
||||
"50": "50 emails",
|
||||
"100": "100 emails"
|
||||
},
|
||||
"external_content": {
|
||||
"label": "External Content",
|
||||
"description": "How to handle images and external content",
|
||||
"ask": "Always ask",
|
||||
"block": "Always block",
|
||||
"allow": "Always allow"
|
||||
}
|
||||
},
|
||||
"composer": {
|
||||
"title": "Composer",
|
||||
"description": "Configure email composition settings",
|
||||
"autosave": {
|
||||
"label": "Auto-save Interval",
|
||||
"description": "How often to save drafts automatically",
|
||||
"30s": "Every 30 seconds",
|
||||
"1m": "Every minute",
|
||||
"2m": "Every 2 minutes",
|
||||
"5m": "Every 5 minutes"
|
||||
},
|
||||
"send_confirmation": {
|
||||
"label": "Send Confirmation",
|
||||
"description": "Ask for confirmation before sending emails"
|
||||
},
|
||||
"default_reply": {
|
||||
"label": "Default Reply Mode",
|
||||
"description": "Default action when clicking reply",
|
||||
"reply": "Reply",
|
||||
"reply_all": "Reply All"
|
||||
}
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privacy & Security",
|
||||
"description": "Manage your privacy and security settings",
|
||||
"external_images": {
|
||||
"label": "Block External Images",
|
||||
"description": "Prevent tracking through external images"
|
||||
},
|
||||
"session_timeout": {
|
||||
"label": "Session Timeout",
|
||||
"description": "Automatically log out after inactivity",
|
||||
"never": "Never",
|
||||
"30m": "30 minutes",
|
||||
"1h": "1 hour",
|
||||
"4h": "4 hours"
|
||||
},
|
||||
"clear_cache": {
|
||||
"label": "Clear Cache",
|
||||
"description": "Remove cached data and temporary files",
|
||||
"button": "Clear Cache",
|
||||
"confirm": "Are you sure you want to clear the cache?",
|
||||
"success": "Cache cleared successfully"
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"title": "Account",
|
||||
"description": "View your account information",
|
||||
"email": {
|
||||
"label": "Email Address",
|
||||
"value": "{{email}}"
|
||||
},
|
||||
"server": {
|
||||
"label": "JMAP Server",
|
||||
"value": "{{server}}"
|
||||
},
|
||||
"storage": {
|
||||
"label": "Storage Usage",
|
||||
"used": "{{used}} of {{total}} used",
|
||||
"percentage": "{{percent}}% used"
|
||||
},
|
||||
"last_sync": {
|
||||
"label": "Last Sync",
|
||||
"value": "{{time}}"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced",
|
||||
"description": "Advanced options and developer settings",
|
||||
"debug_mode": {
|
||||
"label": "Debug Mode",
|
||||
"description": "Enable detailed logging for troubleshooting"
|
||||
},
|
||||
"keyboard_shortcuts": {
|
||||
"label": "Keyboard Shortcuts",
|
||||
"description": "View available keyboard shortcuts",
|
||||
"button": "View Shortcuts"
|
||||
},
|
||||
"reset_settings": {
|
||||
"label": "Reset Settings",
|
||||
"description": "Restore all settings to default values",
|
||||
"button": "Reset to Defaults"
|
||||
},
|
||||
"export_settings": {
|
||||
"label": "Export Settings",
|
||||
"description": "Download your settings as JSON",
|
||||
"button": "Export"
|
||||
},
|
||||
"import_settings": {
|
||||
"label": "Import Settings",
|
||||
"description": "Upload settings from JSON file",
|
||||
"button": "Import"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,5 +199,210 @@
|
||||
"title": "Langue",
|
||||
"english": "English",
|
||||
"french": "Français"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
"back_to_mail": "Retour aux emails",
|
||||
"save_success": "Paramètres enregistrés avec succès",
|
||||
"import_success": "Paramètres importés avec succès",
|
||||
"import_error": "Échec de l'importation des paramètres",
|
||||
"reset_confirm": "Êtes-vous sûr de vouloir réinitialiser tous les paramètres ?",
|
||||
"tabs": {
|
||||
"appearance": "Apparence",
|
||||
"language": "Langue et région",
|
||||
"email": "Comportement email",
|
||||
"composer": "Compositeur",
|
||||
"privacy": "Confidentialité et sécurité",
|
||||
"account": "Compte",
|
||||
"advanced": "Avancé"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Apparence",
|
||||
"description": "Personnalisez l'apparence de votre webmail",
|
||||
"theme": {
|
||||
"label": "Thème",
|
||||
"description": "Choisissez votre schéma de couleurs préféré",
|
||||
"light": "Clair",
|
||||
"dark": "Sombre",
|
||||
"system": "Système"
|
||||
},
|
||||
"font_size": {
|
||||
"label": "Taille de police",
|
||||
"description": "Ajustez la taille du texte pour une meilleure lisibilité",
|
||||
"small": "Petite",
|
||||
"medium": "Moyenne",
|
||||
"large": "Grande"
|
||||
},
|
||||
"list_density": {
|
||||
"label": "Densité de la liste",
|
||||
"description": "Contrôlez l'espacement dans les listes d'emails",
|
||||
"compact": "Compacte",
|
||||
"regular": "Normale",
|
||||
"comfortable": "Confortable"
|
||||
},
|
||||
"animations": {
|
||||
"label": "Activer les animations",
|
||||
"description": "Afficher les transitions et effets fluides"
|
||||
}
|
||||
},
|
||||
"language_region": {
|
||||
"title": "Langue et région",
|
||||
"description": "Configurez vos préférences linguistiques et régionales",
|
||||
"language": {
|
||||
"label": "Langue",
|
||||
"description": "Choisissez votre langue préférée",
|
||||
"english": "English",
|
||||
"french": "Français"
|
||||
},
|
||||
"date_format": {
|
||||
"label": "Format de date",
|
||||
"description": "Comment les dates doivent être affichées",
|
||||
"regional": "Régional",
|
||||
"iso": "ISO 8601",
|
||||
"custom": "Personnalisé"
|
||||
},
|
||||
"time_format": {
|
||||
"label": "Format d'heure",
|
||||
"description": "Choisissez entre 12 heures ou 24 heures",
|
||||
"12h": "12 heures",
|
||||
"24h": "24 heures"
|
||||
},
|
||||
"first_day": {
|
||||
"label": "Premier jour de la semaine",
|
||||
"description": "Commencer la semaine le dimanche ou le lundi",
|
||||
"sunday": "Dimanche",
|
||||
"monday": "Lundi"
|
||||
}
|
||||
},
|
||||
"email_behavior": {
|
||||
"title": "Comportement email",
|
||||
"description": "Configurez la gestion des emails",
|
||||
"mark_read": {
|
||||
"label": "Marquer comme lu",
|
||||
"description": "Quand marquer les emails comme lus à l'ouverture",
|
||||
"instant": "Instantanément",
|
||||
"delay_3s": "Après 3 secondes",
|
||||
"delay_5s": "Après 5 secondes",
|
||||
"never": "Jamais"
|
||||
},
|
||||
"delete_action": {
|
||||
"label": "Action de suppression",
|
||||
"description": "Que se passe-t-il quand vous supprimez un email",
|
||||
"trash": "Déplacer vers la corbeille",
|
||||
"permanent": "Supprimer définitivement"
|
||||
},
|
||||
"show_preview": {
|
||||
"label": "Afficher l'aperçu",
|
||||
"description": "Afficher l'aperçu de l'email dans la liste"
|
||||
},
|
||||
"emails_per_page": {
|
||||
"label": "Emails par page",
|
||||
"description": "Nombre d'emails à charger à la fois",
|
||||
"25": "25 emails",
|
||||
"50": "50 emails",
|
||||
"100": "100 emails"
|
||||
},
|
||||
"external_content": {
|
||||
"label": "Contenu externe",
|
||||
"description": "Comment gérer les images et le contenu externe",
|
||||
"ask": "Toujours demander",
|
||||
"block": "Toujours bloquer",
|
||||
"allow": "Toujours autoriser"
|
||||
}
|
||||
},
|
||||
"composer": {
|
||||
"title": "Compositeur",
|
||||
"description": "Configurez les paramètres de composition d'email",
|
||||
"autosave": {
|
||||
"label": "Intervalle de sauvegarde automatique",
|
||||
"description": "Fréquence de sauvegarde automatique des brouillons",
|
||||
"30s": "Toutes les 30 secondes",
|
||||
"1m": "Toutes les minutes",
|
||||
"2m": "Toutes les 2 minutes",
|
||||
"5m": "Toutes les 5 minutes"
|
||||
},
|
||||
"send_confirmation": {
|
||||
"label": "Confirmation d'envoi",
|
||||
"description": "Demander une confirmation avant d'envoyer les emails"
|
||||
},
|
||||
"default_reply": {
|
||||
"label": "Mode de réponse par défaut",
|
||||
"description": "Action par défaut lors du clic sur répondre",
|
||||
"reply": "Répondre",
|
||||
"reply_all": "Répondre à tous"
|
||||
}
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Confidentialité et sécurité",
|
||||
"description": "Gérez vos paramètres de confidentialité et sécurité",
|
||||
"external_images": {
|
||||
"label": "Bloquer les images externes",
|
||||
"description": "Empêcher le suivi par les images externes"
|
||||
},
|
||||
"session_timeout": {
|
||||
"label": "Délai d'expiration de session",
|
||||
"description": "Déconnexion automatique après inactivité",
|
||||
"never": "Jamais",
|
||||
"30m": "30 minutes",
|
||||
"1h": "1 heure",
|
||||
"4h": "4 heures"
|
||||
},
|
||||
"clear_cache": {
|
||||
"label": "Vider le cache",
|
||||
"description": "Supprimer les données en cache et fichiers temporaires",
|
||||
"button": "Vider le cache",
|
||||
"confirm": "Êtes-vous sûr de vouloir vider le cache ?",
|
||||
"success": "Cache vidé avec succès"
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"title": "Compte",
|
||||
"description": "Consultez les informations de votre compte",
|
||||
"email": {
|
||||
"label": "Adresse email",
|
||||
"value": "{{email}}"
|
||||
},
|
||||
"server": {
|
||||
"label": "Serveur JMAP",
|
||||
"value": "{{server}}"
|
||||
},
|
||||
"storage": {
|
||||
"label": "Utilisation du stockage",
|
||||
"used": "{{used}} sur {{total}} utilisés",
|
||||
"percentage": "{{percent}}% utilisé"
|
||||
},
|
||||
"last_sync": {
|
||||
"label": "Dernière synchronisation",
|
||||
"value": "{{time}}"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Avancé",
|
||||
"description": "Options avancées et paramètres développeur",
|
||||
"debug_mode": {
|
||||
"label": "Mode débogage",
|
||||
"description": "Activer la journalisation détaillée pour le dépannage"
|
||||
},
|
||||
"keyboard_shortcuts": {
|
||||
"label": "Raccourcis clavier",
|
||||
"description": "Voir les raccourcis clavier disponibles",
|
||||
"button": "Voir les raccourcis"
|
||||
},
|
||||
"reset_settings": {
|
||||
"label": "Réinitialiser les paramètres",
|
||||
"description": "Restaurer tous les paramètres par défaut",
|
||||
"button": "Réinitialiser"
|
||||
},
|
||||
"export_settings": {
|
||||
"label": "Exporter les paramètres",
|
||||
"description": "Télécharger vos paramètres au format JSON",
|
||||
"button": "Exporter"
|
||||
},
|
||||
"import_settings": {
|
||||
"label": "Importer les paramètres",
|
||||
"description": "Charger les paramètres depuis un fichier JSON",
|
||||
"button": "Importer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { JMAPClient } from '@/lib/jmap/client';
|
||||
import { useEmailStore } from './email-store';
|
||||
|
||||
interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
@@ -91,6 +92,18 @@ export const useAuthStore = create<AuthState>()(
|
||||
|
||||
// Clear persisted storage
|
||||
localStorage.removeItem('auth-storage');
|
||||
|
||||
// Clear email store state
|
||||
useEmailStore.setState({
|
||||
emails: [],
|
||||
mailboxes: [],
|
||||
selectedEmail: null,
|
||||
selectedMailbox: "",
|
||||
isLoading: false,
|
||||
error: null,
|
||||
searchQuery: "",
|
||||
quota: null,
|
||||
});
|
||||
},
|
||||
|
||||
checkAuth: async () => {
|
||||
|
||||
+23
-3
@@ -1,6 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { Email, Mailbox } from "@/lib/jmap/types";
|
||||
import { JMAPClient } from "@/lib/jmap/client";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
|
||||
interface EmailStore {
|
||||
emails: Email[];
|
||||
@@ -105,7 +106,20 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const mailboxes = await client.getAllMailboxes();
|
||||
set({ mailboxes, isLoading: false });
|
||||
|
||||
// Auto-select inbox if no mailbox is currently selected
|
||||
const currentSelectedMailbox = get().selectedMailbox;
|
||||
if (!currentSelectedMailbox) {
|
||||
// Find inbox from PRIMARY account (not shared accounts)
|
||||
const inboxMailbox = mailboxes.find(m => m.role === 'inbox' && !m.isShared);
|
||||
if (inboxMailbox) {
|
||||
set({ mailboxes, selectedMailbox: inboxMailbox.id, isLoading: false });
|
||||
} else {
|
||||
set({ mailboxes, isLoading: false });
|
||||
}
|
||||
} else {
|
||||
set({ mailboxes, isLoading: false });
|
||||
}
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : "Failed to fetch mailboxes",
|
||||
@@ -127,7 +141,10 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
// Use originalId for JMAP queries (shared mailboxes use namespaced IDs in the store)
|
||||
const jmapMailboxId = mailbox?.originalId || targetMailboxId;
|
||||
|
||||
const result = await client.getEmails(jmapMailboxId, accountId, 50, 0);
|
||||
// Get emails per page from settings
|
||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
||||
|
||||
const result = await client.getEmails(jmapMailboxId, accountId, emailsPerPage, 0);
|
||||
set({
|
||||
emails: result.emails,
|
||||
hasMoreEmails: result.hasMore,
|
||||
@@ -162,7 +179,10 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
// Use originalId for JMAP queries (shared mailboxes use namespaced IDs in the store)
|
||||
const jmapMailboxId = mailbox?.originalId || selectedMailbox;
|
||||
|
||||
const result = await client.getEmails(jmapMailboxId, accountId, 50, emails.length);
|
||||
// Get emails per page from settings
|
||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
||||
|
||||
const result = await client.getEmails(jmapMailboxId, accountId, emailsPerPage, emails.length);
|
||||
|
||||
set({
|
||||
emails: [...emails, ...result.emails],
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export type FontSize = 'small' | 'medium' | 'large';
|
||||
export type ListDensity = 'compact' | 'regular' | 'comfortable';
|
||||
export type DeleteAction = 'trash' | 'permanent';
|
||||
export type ReplyMode = 'reply' | 'replyAll';
|
||||
export type DateFormat = 'regional' | 'iso' | 'custom';
|
||||
export type TimeFormat = '12h' | '24h';
|
||||
export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday
|
||||
export type ExternalContentPolicy = 'ask' | 'block' | 'allow';
|
||||
|
||||
interface SettingsState {
|
||||
// Appearance
|
||||
fontSize: FontSize;
|
||||
listDensity: ListDensity;
|
||||
animationsEnabled: boolean;
|
||||
|
||||
// Language & Region
|
||||
dateFormat: DateFormat;
|
||||
timeFormat: TimeFormat;
|
||||
firstDayOfWeek: FirstDayOfWeek;
|
||||
|
||||
// Email Behavior
|
||||
markAsReadDelay: number; // milliseconds (0 = instant, -1 = never)
|
||||
deleteAction: DeleteAction;
|
||||
showPreview: boolean;
|
||||
emailsPerPage: number;
|
||||
externalContentPolicy: ExternalContentPolicy;
|
||||
|
||||
// Composer
|
||||
autoSaveDraftInterval: number; // milliseconds
|
||||
sendConfirmation: boolean;
|
||||
defaultReplyMode: ReplyMode;
|
||||
|
||||
// Privacy & Security
|
||||
sessionTimeout: number; // minutes (0 = never)
|
||||
|
||||
// Advanced
|
||||
debugMode: boolean;
|
||||
|
||||
// Actions
|
||||
updateSetting: <K extends keyof SettingsState>(
|
||||
key: K,
|
||||
value: SettingsState[K]
|
||||
) => void;
|
||||
resetToDefaults: () => void;
|
||||
exportSettings: () => string;
|
||||
importSettings: (json: string) => boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
// Appearance
|
||||
fontSize: 'medium' as FontSize,
|
||||
listDensity: 'regular' as ListDensity,
|
||||
animationsEnabled: true,
|
||||
|
||||
// Language & Region
|
||||
dateFormat: 'regional' as DateFormat,
|
||||
timeFormat: '24h' as TimeFormat,
|
||||
firstDayOfWeek: 1 as FirstDayOfWeek, // Monday
|
||||
|
||||
// Email Behavior
|
||||
markAsReadDelay: 0, // Instant
|
||||
deleteAction: 'trash' as DeleteAction,
|
||||
showPreview: true,
|
||||
emailsPerPage: 50,
|
||||
externalContentPolicy: 'ask' as ExternalContentPolicy,
|
||||
|
||||
// Composer
|
||||
autoSaveDraftInterval: 60000, // 1 minute
|
||||
sendConfirmation: false,
|
||||
defaultReplyMode: 'reply' as ReplyMode,
|
||||
|
||||
// Privacy & Security
|
||||
sessionTimeout: 0, // Never
|
||||
|
||||
// Advanced
|
||||
debugMode: false,
|
||||
};
|
||||
|
||||
export const useSettingsStore = create<SettingsState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...DEFAULT_SETTINGS,
|
||||
|
||||
updateSetting: (key, value) => {
|
||||
set({ [key]: value });
|
||||
|
||||
// Apply font size to document root
|
||||
if (key === 'fontSize') {
|
||||
applyFontSize(value as FontSize);
|
||||
}
|
||||
|
||||
// Apply list density to document root
|
||||
if (key === 'listDensity') {
|
||||
applyListDensity(value as ListDensity);
|
||||
}
|
||||
|
||||
// Apply animations to document root
|
||||
if (key === 'animationsEnabled') {
|
||||
applyAnimations(value as boolean);
|
||||
}
|
||||
},
|
||||
|
||||
resetToDefaults: () => {
|
||||
set(DEFAULT_SETTINGS);
|
||||
applyFontSize(DEFAULT_SETTINGS.fontSize);
|
||||
applyListDensity(DEFAULT_SETTINGS.listDensity);
|
||||
applyAnimations(DEFAULT_SETTINGS.animationsEnabled);
|
||||
},
|
||||
|
||||
exportSettings: () => {
|
||||
const state = get();
|
||||
const settings = {
|
||||
fontSize: state.fontSize,
|
||||
listDensity: state.listDensity,
|
||||
animationsEnabled: state.animationsEnabled,
|
||||
dateFormat: state.dateFormat,
|
||||
timeFormat: state.timeFormat,
|
||||
firstDayOfWeek: state.firstDayOfWeek,
|
||||
markAsReadDelay: state.markAsReadDelay,
|
||||
deleteAction: state.deleteAction,
|
||||
showPreview: state.showPreview,
|
||||
emailsPerPage: state.emailsPerPage,
|
||||
externalContentPolicy: state.externalContentPolicy,
|
||||
autoSaveDraftInterval: state.autoSaveDraftInterval,
|
||||
sendConfirmation: state.sendConfirmation,
|
||||
defaultReplyMode: state.defaultReplyMode,
|
||||
sessionTimeout: state.sessionTimeout,
|
||||
debugMode: state.debugMode,
|
||||
};
|
||||
return JSON.stringify(settings, null, 2);
|
||||
},
|
||||
|
||||
importSettings: (json: string) => {
|
||||
try {
|
||||
const settings = JSON.parse(json);
|
||||
|
||||
// Validate settings
|
||||
if (typeof settings !== 'object' || settings === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply settings
|
||||
Object.keys(settings).forEach((key) => {
|
||||
if (key in DEFAULT_SETTINGS) {
|
||||
set({ [key]: settings[key] });
|
||||
}
|
||||
});
|
||||
|
||||
// Apply visual settings
|
||||
applyFontSize(get().fontSize);
|
||||
applyListDensity(get().listDensity);
|
||||
applyAnimations(get().animationsEnabled);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to import settings:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'settings-storage',
|
||||
version: 1,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Helper functions to apply settings to DOM
|
||||
function applyFontSize(size: FontSize) {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
const root = document.documentElement;
|
||||
const sizeMap = {
|
||||
small: '14px',
|
||||
medium: '16px',
|
||||
large: '18px',
|
||||
};
|
||||
root.style.setProperty('--font-size-base', sizeMap[size]);
|
||||
}
|
||||
|
||||
function applyListDensity(density: ListDensity) {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
const root = document.documentElement;
|
||||
const densityMap = {
|
||||
compact: '32px',
|
||||
regular: '48px',
|
||||
comfortable: '64px',
|
||||
};
|
||||
root.style.setProperty('--list-item-height', densityMap[density]);
|
||||
}
|
||||
|
||||
function applyAnimations(enabled: boolean) {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
const root = document.documentElement;
|
||||
if (enabled) {
|
||||
root.style.removeProperty('--transition-duration');
|
||||
} else {
|
||||
root.style.setProperty('--transition-duration', '0s');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize settings on load
|
||||
if (typeof window !== 'undefined') {
|
||||
const store = useSettingsStore.getState();
|
||||
applyFontSize(store.fontSize);
|
||||
applyListDensity(store.listDensity);
|
||||
applyAnimations(store.animationsEnabled);
|
||||
}
|
||||
Reference in New Issue
Block a user