* Add lint-fix npm target * Sync eslint+plugins with backend * Add prettier * Ignore no-explicit-any lint rule for now * Silence eslint react warning * Format frontend via prettier * Lint frontend. --------- Co-authored-by: antanst <>
1899 lines
97 KiB
TypeScript
1899 lines
97 KiB
TypeScript
import React, {
|
|
useState,
|
|
useEffect,
|
|
ChangeEvent,
|
|
FormEvent,
|
|
useCallback,
|
|
} from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
InformationCircleIcon,
|
|
EyeIcon,
|
|
EyeSlashIcon,
|
|
UserIcon,
|
|
ClockIcon,
|
|
ChatBubbleLeftRightIcon,
|
|
ShieldCheckIcon,
|
|
LightBulbIcon,
|
|
CogIcon,
|
|
ClipboardDocumentListIcon,
|
|
BoltIcon,
|
|
ChevronRightIcon,
|
|
ExclamationTriangleIcon,
|
|
FaceSmileIcon,
|
|
CheckIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
import { useToast } from '../Shared/ToastContext';
|
|
|
|
interface ProfileSettingsProps {
|
|
currentUser: { id: number; email: string };
|
|
isDarkMode?: boolean;
|
|
toggleDarkMode?: () => void;
|
|
}
|
|
|
|
interface Profile {
|
|
id: number;
|
|
email: string;
|
|
appearance: 'light' | 'dark';
|
|
language: string;
|
|
timezone: string;
|
|
avatar_image: string | null;
|
|
telegram_bot_token: string | null;
|
|
telegram_chat_id: string | null;
|
|
task_summary_enabled: boolean;
|
|
task_summary_frequency: string;
|
|
task_intelligence_enabled: boolean;
|
|
auto_suggest_next_actions_enabled: boolean;
|
|
productivity_assistant_enabled: boolean;
|
|
next_task_suggestion_enabled: boolean;
|
|
pomodoro_enabled: boolean;
|
|
}
|
|
|
|
interface TelegramBotInfo {
|
|
username: string;
|
|
polling_status: any;
|
|
chat_url: string;
|
|
}
|
|
|
|
const formatFrequency = (frequency: string): string => {
|
|
if (frequency.endsWith('h')) {
|
|
const value = frequency.replace('h', '');
|
|
return `${value} ${parseInt(value) === 1 ? 'hour' : 'hours'}`;
|
|
} else if (frequency === 'daily') {
|
|
return '1 day';
|
|
} else if (frequency === 'weekly') {
|
|
return '1 week';
|
|
} else if (frequency === 'weekdays') {
|
|
return 'Weekdays';
|
|
}
|
|
return frequency;
|
|
};
|
|
|
|
const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
|
isDarkMode,
|
|
toggleDarkMode,
|
|
}) => {
|
|
const { t, i18n } = useTranslation();
|
|
const { showSuccessToast, showErrorToast } = useToast();
|
|
|
|
const [activeTab, setActiveTab] = useState('general');
|
|
|
|
// Password visibility state
|
|
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
|
const [showNewPassword, setShowNewPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
|
|
const [profile, setProfile] = useState<Profile | null>(null);
|
|
const [formData, setFormData] = useState<
|
|
Partial<
|
|
Profile & {
|
|
currentPassword: string;
|
|
newPassword: string;
|
|
confirmPassword: string;
|
|
}
|
|
>
|
|
>({
|
|
appearance: isDarkMode ? 'dark' : 'light',
|
|
language: 'en',
|
|
timezone: 'UTC',
|
|
avatar_image: '',
|
|
telegram_bot_token: '',
|
|
task_intelligence_enabled: true,
|
|
task_summary_enabled: false,
|
|
task_summary_frequency: 'daily',
|
|
auto_suggest_next_actions_enabled: true,
|
|
productivity_assistant_enabled: true,
|
|
next_task_suggestion_enabled: true,
|
|
pomodoro_enabled: true,
|
|
currentPassword: '',
|
|
newPassword: '',
|
|
confirmPassword: '',
|
|
});
|
|
const [loading, setLoading] = useState(true);
|
|
const [updateKey, setUpdateKey] = useState(0);
|
|
const [isChangingLanguage, setIsChangingLanguage] = useState(false);
|
|
const [isPolling, setIsPolling] = useState(false);
|
|
const [telegramSetupStatus, setTelegramSetupStatus] = useState<
|
|
'idle' | 'loading' | 'success' | 'error'
|
|
>('idle');
|
|
const [telegramBotInfo, setTelegramBotInfo] =
|
|
useState<TelegramBotInfo | null>(null);
|
|
|
|
const forceUpdate = useCallback(() => {
|
|
setUpdateKey((prevKey) => prevKey + 1);
|
|
}, []);
|
|
|
|
// Password validation
|
|
const validatePasswordForm = (): {
|
|
valid: boolean;
|
|
errors: { [key: string]: string };
|
|
} => {
|
|
const errors: { [key: string]: string } = {};
|
|
|
|
// Only validate if user is trying to change password
|
|
if (
|
|
formData.currentPassword ||
|
|
formData.newPassword ||
|
|
formData.confirmPassword
|
|
) {
|
|
if (!formData.currentPassword) {
|
|
errors.currentPassword = t(
|
|
'profile.currentPasswordRequired',
|
|
'Current password is required'
|
|
);
|
|
}
|
|
|
|
if (!formData.newPassword) {
|
|
errors.newPassword = t(
|
|
'profile.newPasswordRequired',
|
|
'New password is required'
|
|
);
|
|
} else if (formData.newPassword.length < 6) {
|
|
errors.newPassword = t(
|
|
'profile.passwordTooShort',
|
|
'Password must be at least 6 characters'
|
|
);
|
|
}
|
|
|
|
if (formData.newPassword !== formData.confirmPassword) {
|
|
errors.confirmPassword = t(
|
|
'profile.passwordMismatch',
|
|
'Passwords do not match'
|
|
);
|
|
}
|
|
}
|
|
|
|
return { valid: Object.keys(errors).length === 0, errors };
|
|
};
|
|
|
|
const handleChange = (
|
|
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
|
) => {
|
|
const { name, value } = e.target;
|
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
|
};
|
|
|
|
const handleLanguageChange = async (value: string) => {
|
|
try {
|
|
setIsChangingLanguage(true);
|
|
|
|
await i18n.changeLanguage(value);
|
|
|
|
document.documentElement.lang = value;
|
|
|
|
const resources = i18n.getResourceBundle(value, 'translation');
|
|
|
|
if (!resources || Object.keys(resources).length === 0) {
|
|
const loadPath = `/locales/${value}/translation.json`;
|
|
try {
|
|
const response = await fetch(loadPath);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
i18n.addResourceBundle(
|
|
value,
|
|
'translation',
|
|
data,
|
|
true,
|
|
true
|
|
);
|
|
|
|
if (window.forceLanguageReload) {
|
|
window.forceLanguageReload(value);
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore errors loading language resources
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
forceUpdate();
|
|
|
|
const checkAndLoadResources = i18n.getResourceBundle(
|
|
value,
|
|
'translation'
|
|
);
|
|
if (
|
|
!checkAndLoadResources ||
|
|
Object.keys(checkAndLoadResources).length === 0
|
|
) {
|
|
if (window.forceLanguageReload) {
|
|
window.forceLanguageReload(value);
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
if (isChangingLanguage) {
|
|
setIsChangingLanguage(false);
|
|
}
|
|
}, 800);
|
|
}, 200);
|
|
} catch {
|
|
setIsChangingLanguage(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const fetchProfile = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetch('/api/profile');
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
t('profile.fetchError', 'Failed to fetch profile data.')
|
|
);
|
|
}
|
|
|
|
const data = await response.json();
|
|
setProfile(data);
|
|
setFormData({
|
|
appearance:
|
|
data.appearance || (isDarkMode ? 'dark' : 'light'),
|
|
language: data.language || 'en',
|
|
timezone: data.timezone || 'UTC',
|
|
avatar_image: data.avatar_image || '',
|
|
telegram_bot_token: data.telegram_bot_token || '',
|
|
task_intelligence_enabled:
|
|
data.task_intelligence_enabled !== undefined
|
|
? data.task_intelligence_enabled
|
|
: true,
|
|
task_summary_enabled:
|
|
data.task_summary_enabled !== undefined
|
|
? data.task_summary_enabled
|
|
: false,
|
|
task_summary_frequency:
|
|
data.task_summary_frequency || 'daily',
|
|
auto_suggest_next_actions_enabled:
|
|
data.auto_suggest_next_actions_enabled !== undefined
|
|
? data.auto_suggest_next_actions_enabled
|
|
: true,
|
|
productivity_assistant_enabled:
|
|
data.productivity_assistant_enabled !== undefined
|
|
? data.productivity_assistant_enabled
|
|
: true,
|
|
next_task_suggestion_enabled:
|
|
data.next_task_suggestion_enabled !== undefined
|
|
? data.next_task_suggestion_enabled
|
|
: true,
|
|
pomodoro_enabled:
|
|
data.pomodoro_enabled !== undefined
|
|
? data.pomodoro_enabled
|
|
: true,
|
|
});
|
|
|
|
// Note: Task summary status checking functionality removed for now
|
|
|
|
if (data.telegram_bot_token) {
|
|
fetchPollingStatus();
|
|
}
|
|
} catch (error) {
|
|
showErrorToast((error as Error).message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchPollingStatus = async () => {
|
|
try {
|
|
const response = await fetch('/api/telegram/polling-status');
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
t(
|
|
'profile.pollingStatusError',
|
|
'Failed to fetch polling status.'
|
|
)
|
|
);
|
|
}
|
|
|
|
const data = await response.json();
|
|
setIsPolling(data.running);
|
|
|
|
if (data.token_exists && !data.running) {
|
|
handleStartPolling();
|
|
}
|
|
} catch {
|
|
// Ignore errors fetching polling status
|
|
}
|
|
};
|
|
fetchProfile();
|
|
}, []);
|
|
|
|
useEffect(() => {}, [updateKey, i18n.language]);
|
|
|
|
useEffect(() => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
appearance: isDarkMode ? 'dark' : 'light',
|
|
}));
|
|
}, [isDarkMode]);
|
|
|
|
useEffect(() => {
|
|
const handleLanguageChanged = () => {
|
|
forceUpdate();
|
|
};
|
|
|
|
const handleAppLanguageChanged = () => {
|
|
forceUpdate();
|
|
setTimeout(() => {
|
|
setIsChangingLanguage(false);
|
|
}, 300);
|
|
};
|
|
|
|
i18n.on('languageChanged', handleLanguageChanged);
|
|
window.addEventListener(
|
|
'app-language-changed',
|
|
handleAppLanguageChanged as EventListener
|
|
);
|
|
|
|
return () => {
|
|
i18n.off('languageChanged', handleLanguageChanged);
|
|
window.removeEventListener(
|
|
'app-language-changed',
|
|
handleAppLanguageChanged as EventListener
|
|
);
|
|
};
|
|
}, []);
|
|
|
|
const handleSetupTelegram = async () => {
|
|
setTelegramSetupStatus('loading');
|
|
setTelegramBotInfo(null);
|
|
|
|
try {
|
|
if (
|
|
!formData.telegram_bot_token ||
|
|
!formData.telegram_bot_token.includes(':')
|
|
) {
|
|
throw new Error(t('profile.invalidTelegramToken'));
|
|
}
|
|
|
|
const response = await fetch('/api/telegram/setup', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ token: formData.telegram_bot_token }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || t('profile.telegramSetupFailed'));
|
|
}
|
|
|
|
const data = await response.json();
|
|
setTelegramSetupStatus('success');
|
|
showSuccessToast(
|
|
t(
|
|
'profile.telegramSetupSuccess',
|
|
'Telegram bot configured successfully!'
|
|
)
|
|
);
|
|
|
|
if (data.bot) {
|
|
setTelegramBotInfo(data.bot);
|
|
setIsPolling(true);
|
|
|
|
if (!data.bot.polling_status?.running) {
|
|
setTimeout(() => {
|
|
handleStartPolling();
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
const botUsername =
|
|
data.bot?.username || formData.telegram_bot_token.split(':')[0];
|
|
|
|
window.open(`https://t.me/${botUsername}`, '_blank');
|
|
} catch (error) {
|
|
setTelegramSetupStatus('error');
|
|
showErrorToast((error as Error).message);
|
|
}
|
|
};
|
|
|
|
const handleStartPolling = async () => {
|
|
try {
|
|
const response = await fetch('/api/telegram/start-polling', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || t('profile.startPollingFailed'));
|
|
}
|
|
|
|
const data = await response.json();
|
|
setIsPolling(true);
|
|
showSuccessToast(t('profile.pollingStarted'));
|
|
|
|
if (telegramBotInfo) {
|
|
setTelegramBotInfo({
|
|
...telegramBotInfo,
|
|
polling_status: data.status,
|
|
});
|
|
}
|
|
} catch {
|
|
showErrorToast(t('profile.pollingError'));
|
|
}
|
|
};
|
|
|
|
const handleStopPolling = async () => {
|
|
try {
|
|
const response = await fetch('/api/telegram/stop-polling', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || t('profile.stopPollingFailed'));
|
|
}
|
|
|
|
const data = await response.json();
|
|
setIsPolling(false);
|
|
showSuccessToast(
|
|
t('profile.pollingStopped', 'Polling stopped successfully.')
|
|
);
|
|
|
|
if (telegramBotInfo) {
|
|
setTelegramBotInfo({
|
|
...telegramBotInfo,
|
|
polling_status: data.status,
|
|
});
|
|
}
|
|
} catch {
|
|
showErrorToast(t('profile.pollingError'));
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
// Check if user is trying to change password
|
|
const isPasswordChange =
|
|
formData.currentPassword ||
|
|
formData.newPassword ||
|
|
formData.confirmPassword;
|
|
|
|
// Only validate password if user is trying to change password
|
|
if (isPasswordChange) {
|
|
const passwordValidation = validatePasswordForm();
|
|
if (!passwordValidation.valid) {
|
|
showErrorToast(Object.values(passwordValidation.errors)[0]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Prepare data to send - exclude password fields if not changing password
|
|
const dataToSend = { ...formData };
|
|
if (!isPasswordChange) {
|
|
delete dataToSend.currentPassword;
|
|
delete dataToSend.newPassword;
|
|
delete dataToSend.confirmPassword;
|
|
}
|
|
|
|
const response = await fetch('/api/profile', {
|
|
method: 'PATCH',
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json',
|
|
},
|
|
body: JSON.stringify(dataToSend),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || 'Failed to update profile.');
|
|
}
|
|
|
|
const updatedProfile: Profile = await response.json();
|
|
setProfile(updatedProfile);
|
|
|
|
// Update formData to reflect the saved changes, preserving any fields not in response
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
appearance:
|
|
updatedProfile.appearance || prev.appearance || 'light',
|
|
language: updatedProfile.language || prev.language || 'en',
|
|
timezone: updatedProfile.timezone || prev.timezone || 'UTC',
|
|
avatar_image:
|
|
updatedProfile.avatar_image !== undefined
|
|
? updatedProfile.avatar_image
|
|
: prev.avatar_image || '',
|
|
telegram_bot_token:
|
|
updatedProfile.telegram_bot_token !== undefined
|
|
? updatedProfile.telegram_bot_token
|
|
: prev.telegram_bot_token || '',
|
|
task_intelligence_enabled:
|
|
updatedProfile.task_intelligence_enabled !== undefined
|
|
? updatedProfile.task_intelligence_enabled
|
|
: prev.task_intelligence_enabled !== undefined
|
|
? prev.task_intelligence_enabled
|
|
: true,
|
|
task_summary_enabled:
|
|
updatedProfile.task_summary_enabled !== undefined
|
|
? updatedProfile.task_summary_enabled
|
|
: prev.task_summary_enabled !== undefined
|
|
? prev.task_summary_enabled
|
|
: false,
|
|
task_summary_frequency:
|
|
updatedProfile.task_summary_frequency ||
|
|
prev.task_summary_frequency ||
|
|
'daily',
|
|
auto_suggest_next_actions_enabled:
|
|
updatedProfile.auto_suggest_next_actions_enabled !==
|
|
undefined
|
|
? updatedProfile.auto_suggest_next_actions_enabled
|
|
: prev.auto_suggest_next_actions_enabled !== undefined
|
|
? prev.auto_suggest_next_actions_enabled
|
|
: true,
|
|
productivity_assistant_enabled:
|
|
updatedProfile.productivity_assistant_enabled !== undefined
|
|
? updatedProfile.productivity_assistant_enabled
|
|
: prev.productivity_assistant_enabled !== undefined
|
|
? prev.productivity_assistant_enabled
|
|
: true,
|
|
next_task_suggestion_enabled:
|
|
updatedProfile.next_task_suggestion_enabled !== undefined
|
|
? updatedProfile.next_task_suggestion_enabled
|
|
: prev.next_task_suggestion_enabled !== undefined
|
|
? prev.next_task_suggestion_enabled
|
|
: true,
|
|
pomodoro_enabled:
|
|
updatedProfile.pomodoro_enabled !== undefined
|
|
? updatedProfile.pomodoro_enabled
|
|
: prev.pomodoro_enabled !== undefined
|
|
? prev.pomodoro_enabled
|
|
: true,
|
|
}));
|
|
|
|
// Apply appearance change after save
|
|
if (
|
|
updatedProfile.appearance !== (isDarkMode ? 'dark' : 'light') &&
|
|
toggleDarkMode
|
|
) {
|
|
toggleDarkMode();
|
|
}
|
|
|
|
// Apply language change after save
|
|
if (updatedProfile.language !== i18n.language) {
|
|
await handleLanguageChange(updatedProfile.language);
|
|
}
|
|
|
|
// Notify other components about Pomodoro setting change
|
|
if (updatedProfile.pomodoro_enabled !== undefined) {
|
|
window.dispatchEvent(
|
|
new CustomEvent('pomodoroSettingChanged', {
|
|
detail: { enabled: updatedProfile.pomodoro_enabled },
|
|
})
|
|
);
|
|
}
|
|
|
|
// Clear password fields on successful save
|
|
if (isPasswordChange) {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
currentPassword: '',
|
|
newPassword: '',
|
|
confirmPassword: '',
|
|
}));
|
|
}
|
|
|
|
const successMessage = isPasswordChange
|
|
? t(
|
|
'profile.passwordChangeSuccess',
|
|
'Password changed successfully!'
|
|
)
|
|
: t('profile.successMessage', 'Profile updated successfully!');
|
|
showSuccessToast(successMessage);
|
|
} catch (err) {
|
|
showErrorToast((err as Error).message);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-screen bg-gray-100 dark:bg-gray-900">
|
|
<div className="text-xl font-semibold text-gray-700 dark:text-gray-200">
|
|
{t('common.loading')}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const tabs = [
|
|
{
|
|
id: 'general',
|
|
name: t('profile.tabs.general', 'General'),
|
|
icon: 'user',
|
|
},
|
|
{
|
|
id: 'security',
|
|
name: t('profile.tabs.security', 'Security'),
|
|
icon: 'shield',
|
|
},
|
|
{
|
|
id: 'productivity',
|
|
name: t('profile.tabs.productivity', 'Productivity'),
|
|
icon: 'clock',
|
|
},
|
|
{
|
|
id: 'telegram',
|
|
name: t('profile.tabs.telegram', 'Telegram'),
|
|
icon: 'chat',
|
|
},
|
|
{
|
|
id: 'ai',
|
|
name: t('profile.tabs.ai', 'AI Features'),
|
|
icon: 'sparkles',
|
|
},
|
|
];
|
|
|
|
const renderTabIcon = (iconType: string) => {
|
|
switch (iconType) {
|
|
case 'user':
|
|
return <UserIcon className="w-5 h-5" />;
|
|
case 'clock':
|
|
return <ClockIcon className="w-5 h-5" />;
|
|
case 'chat':
|
|
return <ChatBubbleLeftRightIcon className="w-5 h-5" />;
|
|
case 'shield':
|
|
return <ShieldCheckIcon className="w-5 h-5" />;
|
|
case 'sparkles':
|
|
return <LightBulbIcon className="w-5 h-5" />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="max-w-5xl mx-auto p-6"
|
|
key={`profile-settings-${updateKey}`}
|
|
>
|
|
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-6">
|
|
{t('profile.title')}
|
|
</h2>
|
|
|
|
{/* Navigation Tabs */}
|
|
<div className="mb-8">
|
|
<div className="border-b border-gray-200 dark:border-gray-700">
|
|
<nav className="-mb-px flex space-x-8">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
type="button"
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`group inline-flex items-center py-2 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === tab.id
|
|
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
|
}`}
|
|
>
|
|
<span className="mr-2">
|
|
{renderTabIcon(tab.icon)}
|
|
</span>
|
|
{tab.name}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-8">
|
|
{/* General Tab */}
|
|
{activeTab === 'general' && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700">
|
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-6 flex items-center">
|
|
<UserIcon className="w-6 h-6 mr-3 text-blue-500" />
|
|
{t(
|
|
'profile.accountSettings',
|
|
'Account & Preferences'
|
|
)}
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
{t('profile.appearance')}
|
|
</label>
|
|
<select
|
|
name="appearance"
|
|
value={formData.appearance}
|
|
onChange={handleChange}
|
|
className="block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="light">
|
|
{t('profile.lightMode', 'Light')}
|
|
</option>
|
|
<option value="dark">
|
|
{t('profile.darkMode', 'Dark')}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
{t('profile.language')}
|
|
</label>
|
|
<select
|
|
name="language"
|
|
value={formData.language}
|
|
onChange={handleChange}
|
|
className="block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="en">
|
|
{t('profile.english')}
|
|
</option>
|
|
<option value="es">
|
|
{t('profile.spanish')}
|
|
</option>
|
|
<option value="el">
|
|
{t('profile.greek')}
|
|
</option>
|
|
<option value="jp">
|
|
{t('profile.japanese')}
|
|
</option>
|
|
<option value="ua">
|
|
{t('profile.ukrainian')}
|
|
</option>
|
|
<option value="de">
|
|
{t('profile.deutsch')}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
{t('profile.timezone')}
|
|
</label>
|
|
<select
|
|
name="timezone"
|
|
value={formData.timezone}
|
|
onChange={handleChange}
|
|
className="block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="UTC">UTC</option>
|
|
|
|
{/* Americas */}
|
|
<optgroup label="Americas">
|
|
<option value="America/New_York">
|
|
Eastern Time (New York)
|
|
</option>
|
|
<option value="America/Chicago">
|
|
Central Time (Chicago)
|
|
</option>
|
|
<option value="America/Denver">
|
|
Mountain Time (Denver)
|
|
</option>
|
|
<option value="America/Los_Angeles">
|
|
Pacific Time (Los Angeles)
|
|
</option>
|
|
<option value="America/Anchorage">
|
|
Alaska Time (Anchorage)
|
|
</option>
|
|
<option value="Pacific/Honolulu">
|
|
Hawaii Time (Honolulu)
|
|
</option>
|
|
<option value="America/Toronto">
|
|
Eastern Time (Toronto)
|
|
</option>
|
|
<option value="America/Vancouver">
|
|
Pacific Time (Vancouver)
|
|
</option>
|
|
<option value="America/Mexico_City">
|
|
Central Time (Mexico City)
|
|
</option>
|
|
<option value="America/Sao_Paulo">
|
|
Brasília Time (São Paulo)
|
|
</option>
|
|
<option value="America/Argentina/Buenos_Aires">
|
|
Argentina Time (Buenos Aires)
|
|
</option>
|
|
<option value="America/Lima">
|
|
Peru Time (Lima)
|
|
</option>
|
|
<option value="America/Bogota">
|
|
Colombia Time (Bogotá)
|
|
</option>
|
|
<option value="America/Caracas">
|
|
Venezuela Time (Caracas)
|
|
</option>
|
|
<option value="America/Santiago">
|
|
Chile Time (Santiago)
|
|
</option>
|
|
</optgroup>
|
|
|
|
{/* Europe */}
|
|
<optgroup label="Europe">
|
|
<option value="Europe/London">
|
|
Greenwich Mean Time (London)
|
|
</option>
|
|
<option value="Europe/Dublin">
|
|
Greenwich Mean Time (Dublin)
|
|
</option>
|
|
<option value="Europe/Lisbon">
|
|
Western European Time (Lisbon)
|
|
</option>
|
|
<option value="Europe/Paris">
|
|
Central European Time (Paris)
|
|
</option>
|
|
<option value="Europe/Berlin">
|
|
Central European Time (Berlin)
|
|
</option>
|
|
<option value="Europe/Madrid">
|
|
Central European Time (Madrid)
|
|
</option>
|
|
<option value="Europe/Rome">
|
|
Central European Time (Rome)
|
|
</option>
|
|
<option value="Europe/Amsterdam">
|
|
Central European Time (Amsterdam)
|
|
</option>
|
|
<option value="Europe/Brussels">
|
|
Central European Time (Brussels)
|
|
</option>
|
|
<option value="Europe/Vienna">
|
|
Central European Time (Vienna)
|
|
</option>
|
|
<option value="Europe/Zurich">
|
|
Central European Time (Zurich)
|
|
</option>
|
|
<option value="Europe/Prague">
|
|
Central European Time (Prague)
|
|
</option>
|
|
<option value="Europe/Warsaw">
|
|
Central European Time (Warsaw)
|
|
</option>
|
|
<option value="Europe/Stockholm">
|
|
Central European Time (Stockholm)
|
|
</option>
|
|
<option value="Europe/Oslo">
|
|
Central European Time (Oslo)
|
|
</option>
|
|
<option value="Europe/Copenhagen">
|
|
Central European Time (Copenhagen)
|
|
</option>
|
|
<option value="Europe/Helsinki">
|
|
Eastern European Time (Helsinki)
|
|
</option>
|
|
<option value="Europe/Athens">
|
|
Eastern European Time (Athens)
|
|
</option>
|
|
<option value="Europe/Kiev">
|
|
Eastern European Time (Kiev)
|
|
</option>
|
|
<option value="Europe/Moscow">
|
|
Moscow Time (Moscow)
|
|
</option>
|
|
<option value="Europe/Istanbul">
|
|
Turkey Time (Istanbul)
|
|
</option>
|
|
</optgroup>
|
|
|
|
{/* Asia */}
|
|
<optgroup label="Asia">
|
|
<option value="Asia/Dubai">
|
|
Gulf Standard Time (Dubai)
|
|
</option>
|
|
<option value="Asia/Tehran">
|
|
Iran Standard Time (Tehran)
|
|
</option>
|
|
<option value="Asia/Yerevan">
|
|
Armenia Time (Yerevan)
|
|
</option>
|
|
<option value="Asia/Baku">
|
|
Azerbaijan Time (Baku)
|
|
</option>
|
|
<option value="Asia/Karachi">
|
|
Pakistan Standard Time (Karachi)
|
|
</option>
|
|
<option value="Asia/Kolkata">
|
|
India Standard Time (Mumbai/Delhi)
|
|
</option>
|
|
<option value="Asia/Kathmandu">
|
|
Nepal Time (Kathmandu)
|
|
</option>
|
|
<option value="Asia/Dhaka">
|
|
Bangladesh Standard Time (Dhaka)
|
|
</option>
|
|
<option value="Asia/Yangon">
|
|
Myanmar Time (Yangon)
|
|
</option>
|
|
<option value="Asia/Bangkok">
|
|
Indochina Time (Bangkok)
|
|
</option>
|
|
<option value="Asia/Ho_Chi_Minh">
|
|
Indochina Time (Ho Chi Minh)
|
|
</option>
|
|
<option value="Asia/Jakarta">
|
|
Western Indonesia Time (Jakarta)
|
|
</option>
|
|
<option value="Asia/Kuala_Lumpur">
|
|
Malaysia Time (Kuala Lumpur)
|
|
</option>
|
|
<option value="Asia/Singapore">
|
|
Singapore Standard Time (Singapore)
|
|
</option>
|
|
<option value="Asia/Manila">
|
|
Philippines Time (Manila)
|
|
</option>
|
|
<option value="Asia/Hong_Kong">
|
|
Hong Kong Time (Hong Kong)
|
|
</option>
|
|
<option value="Asia/Shanghai">
|
|
China Standard Time
|
|
(Beijing/Shanghai)
|
|
</option>
|
|
<option value="Asia/Taipei">
|
|
China Standard Time (Taipei)
|
|
</option>
|
|
<option value="Asia/Tokyo">
|
|
Japan Standard Time (Tokyo)
|
|
</option>
|
|
<option value="Asia/Seoul">
|
|
Korea Standard Time (Seoul)
|
|
</option>
|
|
<option value="Asia/Vladivostok">
|
|
Vladivostok Time (Vladivostok)
|
|
</option>
|
|
</optgroup>
|
|
|
|
{/* Africa */}
|
|
<optgroup label="Africa">
|
|
<option value="Africa/Casablanca">
|
|
Western European Time (Casablanca)
|
|
</option>
|
|
<option value="Africa/Lagos">
|
|
West Africa Time (Lagos)
|
|
</option>
|
|
<option value="Africa/Cairo">
|
|
Eastern European Time (Cairo)
|
|
</option>
|
|
<option value="Africa/Johannesburg">
|
|
South Africa Standard Time
|
|
(Johannesburg)
|
|
</option>
|
|
<option value="Africa/Nairobi">
|
|
East Africa Time (Nairobi)
|
|
</option>
|
|
<option value="Africa/Addis_Ababa">
|
|
East Africa Time (Addis Ababa)
|
|
</option>
|
|
</optgroup>
|
|
|
|
{/* Oceania */}
|
|
<optgroup label="Oceania">
|
|
<option value="Australia/Perth">
|
|
Australian Western Standard Time
|
|
(Perth)
|
|
</option>
|
|
<option value="Australia/Adelaide">
|
|
Australian Central Standard Time
|
|
(Adelaide)
|
|
</option>
|
|
<option value="Australia/Darwin">
|
|
Australian Central Standard Time
|
|
(Darwin)
|
|
</option>
|
|
<option value="Australia/Brisbane">
|
|
Australian Eastern Standard Time
|
|
(Brisbane)
|
|
</option>
|
|
<option value="Australia/Sydney">
|
|
Australian Eastern Standard Time
|
|
(Sydney)
|
|
</option>
|
|
<option value="Australia/Melbourne">
|
|
Australian Eastern Standard Time
|
|
(Melbourne)
|
|
</option>
|
|
<option value="Pacific/Auckland">
|
|
New Zealand Standard Time (Auckland)
|
|
</option>
|
|
<option value="Pacific/Fiji">
|
|
Fiji Time (Suva)
|
|
</option>
|
|
<option value="Pacific/Guam">
|
|
Chamorro Standard Time (Guam)
|
|
</option>
|
|
</optgroup>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Security Tab */}
|
|
{activeTab === 'security' && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700">
|
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-6 flex items-center">
|
|
<ShieldCheckIcon className="w-6 h-6 mr-3 text-red-500" />
|
|
{t('profile.security', 'Security Settings')}
|
|
</h3>
|
|
|
|
{/* Password Change Section */}
|
|
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-3 flex items-center">
|
|
<UserIcon className="w-5 h-5 mr-2 text-blue-500" />
|
|
{t('profile.changePassword', 'Change Password')}
|
|
</h4>
|
|
|
|
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-800 rounded text-blue-800 dark:text-blue-200">
|
|
<p className="text-sm">
|
|
<InformationCircleIcon className="w-4 h-4 inline mr-1" />
|
|
{t(
|
|
'profile.passwordChangeOptional',
|
|
'Leave password fields empty to update other settings without changing your password.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
{t(
|
|
'profile.currentPassword',
|
|
'Current Password'
|
|
)}
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={
|
|
showCurrentPassword
|
|
? 'text'
|
|
: 'password'
|
|
}
|
|
name="currentPassword"
|
|
value={
|
|
formData.currentPassword || ''
|
|
}
|
|
onChange={handleChange}
|
|
className="block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm px-3 py-2 pr-10 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder={t(
|
|
'profile.enterCurrentPassword',
|
|
'Enter your current password'
|
|
)}
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
onClick={() =>
|
|
setShowCurrentPassword(
|
|
!showCurrentPassword
|
|
)
|
|
}
|
|
>
|
|
{showCurrentPassword ? (
|
|
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
|
|
) : (
|
|
<EyeIcon className="h-5 w-5 text-gray-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
{t(
|
|
'profile.newPassword',
|
|
'New Password'
|
|
)}
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={
|
|
showNewPassword
|
|
? 'text'
|
|
: 'password'
|
|
}
|
|
name="newPassword"
|
|
value={formData.newPassword || ''}
|
|
onChange={handleChange}
|
|
className="block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm px-3 py-2 pr-10 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder={t(
|
|
'profile.enterNewPassword',
|
|
'Enter your new password'
|
|
)}
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
onClick={() =>
|
|
setShowNewPassword(
|
|
!showNewPassword
|
|
)
|
|
}
|
|
>
|
|
{showNewPassword ? (
|
|
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
|
|
) : (
|
|
<EyeIcon className="h-5 w-5 text-gray-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
{t(
|
|
'profile.confirmPassword',
|
|
'Confirm New Password'
|
|
)}
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={
|
|
showConfirmPassword
|
|
? 'text'
|
|
: 'password'
|
|
}
|
|
name="confirmPassword"
|
|
value={
|
|
formData.confirmPassword || ''
|
|
}
|
|
onChange={handleChange}
|
|
className="block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm px-3 py-2 pr-10 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder={t(
|
|
'profile.confirmNewPassword',
|
|
'Confirm your new password'
|
|
)}
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
onClick={() =>
|
|
setShowConfirmPassword(
|
|
!showConfirmPassword
|
|
)
|
|
}
|
|
>
|
|
{showConfirmPassword ? (
|
|
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
|
|
) : (
|
|
<EyeIcon className="h-5 w-5 text-gray-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
{t(
|
|
'profile.passwordChangeNote',
|
|
'Password changes will be saved when you click "Save Changes" at the bottom of the form.'
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Productivity Tab */}
|
|
{activeTab === 'productivity' && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700">
|
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-6 flex items-center">
|
|
<ClockIcon className="w-6 h-6 mr-3 text-green-500" />
|
|
{t(
|
|
'profile.productivityFeatures',
|
|
'Productivity Features'
|
|
)}
|
|
</h3>
|
|
|
|
<div className="space-y-6">
|
|
{/* Pomodoro Timer */}
|
|
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<div>
|
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{t(
|
|
'profile.enablePomodoro',
|
|
'Enable Pomodoro Timer'
|
|
)}
|
|
</label>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
{t(
|
|
'profile.pomodoroDescription',
|
|
'Enable the Pomodoro timer in the navigation bar for focused work sessions.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div
|
|
className={`relative inline-block w-12 h-6 transition-colors duration-200 ease-in-out rounded-full cursor-pointer ${
|
|
formData.pomodoro_enabled
|
|
? 'bg-blue-500'
|
|
: 'bg-gray-300 dark:bg-gray-600'
|
|
}`}
|
|
onClick={() => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
pomodoro_enabled:
|
|
!prev.pomodoro_enabled,
|
|
}));
|
|
}}
|
|
>
|
|
<span
|
|
className={`absolute left-0 top-0 bottom-0 m-1 w-4 h-4 transition-transform duration-200 ease-in-out transform bg-white rounded-full ${
|
|
formData.pomodoro_enabled
|
|
? 'translate-x-6'
|
|
: 'translate-x-0'
|
|
}`}
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Telegram Tab */}
|
|
{activeTab === 'telegram' && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-blue-300 dark:border-blue-700 mb-8">
|
|
<h3 className="text-xl font-semibold text-blue-700 dark:text-blue-300 mb-6 flex items-center">
|
|
<ChatBubbleLeftRightIcon className="w-6 h-6 mr-3 text-blue-500" />
|
|
{t(
|
|
'profile.telegramIntegration',
|
|
'Telegram Integration'
|
|
)}
|
|
</h3>
|
|
|
|
{/* Bot Setup Subsection */}
|
|
<div className="mb-8 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-3 flex items-center">
|
|
<CogIcon className="w-5 h-5 mr-2 text-blue-500" />
|
|
{t('profile.botSetup', 'Bot Setup')}
|
|
</h4>
|
|
|
|
<div className="space-y-4">
|
|
<div className="text-sm text-gray-600 dark:text-gray-300 flex items-start">
|
|
<InformationCircleIcon className="h-5 w-5 mr-2 flex-shrink-0 text-blue-500" />
|
|
<p>
|
|
{t(
|
|
'profile.telegramDescription',
|
|
'Connect your Tududi account to a Telegram bot to add items to your inbox via Telegram messages.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{t(
|
|
'profile.telegramBotToken',
|
|
'Telegram Bot Token'
|
|
)}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="telegram_bot_token"
|
|
value={
|
|
formData.telegram_bot_token || ''
|
|
}
|
|
onChange={handleChange}
|
|
placeholder="123456789:ABCDefGhIJKlmNoPQRsTUVwxyZ"
|
|
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
|
/>
|
|
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
|
{t(
|
|
'profile.telegramTokenDescription',
|
|
'Create a bot with @BotFather on Telegram and paste the token here.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
{profile?.telegram_chat_id && (
|
|
<div className="p-2 bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-800 rounded text-green-800 dark:text-green-200">
|
|
<p className="text-sm">
|
|
{t(
|
|
'profile.telegramConnected',
|
|
'Your Telegram account is connected! Send messages to your bot to add items to your Tududi inbox.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{telegramBotInfo && (
|
|
<div className="p-2 bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-800 rounded text-blue-800 dark:text-blue-200">
|
|
<p className="font-medium mb-2">
|
|
{t(
|
|
'profile.botConfigured',
|
|
'Bot configured successfully!'
|
|
)}
|
|
</p>
|
|
<div className="text-sm space-y-1">
|
|
<p>
|
|
<span className="font-semibold">
|
|
{t(
|
|
'profile.botUsername',
|
|
'Bot Username:'
|
|
)}{' '}
|
|
</span>
|
|
@{telegramBotInfo.username}
|
|
</p>
|
|
<div className="mt-2">
|
|
<p className="font-semibold mb-1">
|
|
{t(
|
|
'profile.pollingStatus',
|
|
'Polling Status:'
|
|
)}{' '}
|
|
</p>
|
|
<div className="flex items-center mb-2">
|
|
<div
|
|
className={`w-3 h-3 rounded-full mr-2 ${isPolling ? 'bg-green-500' : 'bg-red-500'}`}
|
|
></div>
|
|
<span>
|
|
{isPolling
|
|
? t(
|
|
'profile.pollingActive'
|
|
)
|
|
: t(
|
|
'profile.pollingInactive'
|
|
)}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs mb-2">
|
|
{t(
|
|
'profile.pollingNote',
|
|
'Polling periodically checks for new messages from Telegram and adds them to your inbox.'
|
|
)}
|
|
</p>
|
|
<div className="flex flex-wrap gap-2 mt-2">
|
|
{isPolling ? (
|
|
<button
|
|
onClick={
|
|
handleStopPolling
|
|
}
|
|
className="px-3 py-1 bg-red-600 text-white dark:bg-red-700 rounded text-sm hover:bg-red-700 dark:hover:bg-red-800"
|
|
>
|
|
{t(
|
|
'profile.stopPolling',
|
|
'Stop Polling'
|
|
)}
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={
|
|
handleStartPolling
|
|
}
|
|
className="px-3 py-1 bg-blue-600 text-white dark:bg-blue-700 rounded text-sm hover:bg-blue-700 dark:hover:bg-blue-800"
|
|
>
|
|
{t(
|
|
'profile.startPolling',
|
|
'Start Polling'
|
|
)}
|
|
</button>
|
|
)}
|
|
<a
|
|
href={
|
|
telegramBotInfo.chat_url
|
|
}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="px-3 py-1 bg-green-600 text-white dark:bg-green-700 rounded text-sm hover:bg-green-700 dark:hover:bg-green-800"
|
|
>
|
|
{t(
|
|
'profile.openTelegram',
|
|
'Open in Telegram'
|
|
)}
|
|
</a>
|
|
<button
|
|
onClick={async () => {
|
|
try {
|
|
const testMessage =
|
|
prompt(
|
|
'Enter a test message:'
|
|
);
|
|
if (
|
|
testMessage
|
|
) {
|
|
const response =
|
|
await fetch(
|
|
`/api/telegram/test/${profile?.id}`,
|
|
{
|
|
method: 'POST',
|
|
headers:
|
|
{
|
|
'Content-Type':
|
|
'application/json',
|
|
},
|
|
body: JSON.stringify(
|
|
{
|
|
text: testMessage,
|
|
}
|
|
),
|
|
}
|
|
);
|
|
const result =
|
|
await response.json();
|
|
if (
|
|
result.success
|
|
) {
|
|
showSuccessToast(
|
|
t(
|
|
'profile.testMessageSent',
|
|
'Test message sent successfully!'
|
|
)
|
|
);
|
|
} else {
|
|
showErrorToast(
|
|
t(
|
|
'profile.testMessageFailed',
|
|
'Failed to send test message.'
|
|
)
|
|
);
|
|
}
|
|
}
|
|
} catch {
|
|
showErrorToast(
|
|
t(
|
|
'profile.testMessageError',
|
|
'Error sending test message.'
|
|
)
|
|
);
|
|
}
|
|
}}
|
|
className="px-3 py-1 bg-purple-600 text-white dark:bg-purple-700 rounded text-sm hover:bg-purple-700 dark:hover:bg-purple-800"
|
|
>
|
|
{t(
|
|
'profile.testTelegramMessage',
|
|
'Test Telegram'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleSetupTelegram}
|
|
disabled={
|
|
!formData.telegram_bot_token ||
|
|
telegramSetupStatus === 'loading'
|
|
}
|
|
className={`px-4 py-2 rounded-md ${
|
|
!formData.telegram_bot_token ||
|
|
telegramSetupStatus === 'loading'
|
|
? 'bg-gray-300 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed'
|
|
: 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600'
|
|
}`}
|
|
>
|
|
{telegramSetupStatus === 'loading'
|
|
? t(
|
|
'profile.settingUp',
|
|
'Setting up...'
|
|
)
|
|
: t(
|
|
'profile.setupTelegram',
|
|
'Setup Telegram'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Task Summary Notifications Subsection */}
|
|
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-3 flex items-center">
|
|
<ClipboardDocumentListIcon className="w-5 h-5 mr-2 text-green-500" />
|
|
{t(
|
|
'profile.taskSummaryNotifications',
|
|
'Task Summary Notifications'
|
|
)}
|
|
</h4>
|
|
|
|
<div className="mb-4 text-sm text-gray-600 dark:text-gray-300 flex items-start">
|
|
<InformationCircleIcon className="h-5 w-5 mr-2 flex-shrink-0 text-blue-500" />
|
|
<p>
|
|
{t(
|
|
'profile.taskSummaryDescription',
|
|
'Receive regular summaries of your tasks via Telegram. This feature requires your Telegram integration to be set up.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{t(
|
|
'profile.enableTaskSummary',
|
|
'Enable Task Summaries'
|
|
)}
|
|
</label>
|
|
<div
|
|
className={`relative inline-block w-12 h-6 transition-colors duration-200 ease-in-out rounded-full cursor-pointer ${
|
|
formData.task_summary_enabled
|
|
? 'bg-blue-500'
|
|
: 'bg-gray-300 dark:bg-gray-600'
|
|
}`}
|
|
onClick={() => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
task_summary_enabled:
|
|
!prev.task_summary_enabled,
|
|
}));
|
|
}}
|
|
>
|
|
<span
|
|
className={`absolute left-0 top-0 bottom-0 m-1 w-4 h-4 transition-transform duration-200 ease-in-out transform bg-white rounded-full ${
|
|
formData.task_summary_enabled
|
|
? 'translate-x-6'
|
|
: 'translate-x-0'
|
|
}`}
|
|
></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
{t(
|
|
'profile.summaryFrequency',
|
|
'Summary Frequency'
|
|
)}
|
|
</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{[
|
|
'1h',
|
|
'2h',
|
|
'4h',
|
|
'8h',
|
|
'12h',
|
|
'daily',
|
|
'weekly',
|
|
].map((frequency) => (
|
|
<button
|
|
key={frequency}
|
|
type="button"
|
|
className={`px-3 py-1.5 text-sm rounded-full ${
|
|
formData.task_summary_frequency ===
|
|
frequency
|
|
? 'bg-blue-500 text-white'
|
|
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
|
}`}
|
|
onClick={() => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
task_summary_frequency:
|
|
frequency,
|
|
}));
|
|
}}
|
|
>
|
|
{t(
|
|
`profile.frequency.${frequency}`,
|
|
formatFrequency(frequency)
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
|
{t(
|
|
'profile.frequencyHelp',
|
|
'Choose how often you want to receive task summaries.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<button
|
|
type="button"
|
|
disabled={
|
|
!profile?.telegram_bot_token ||
|
|
!profile?.telegram_chat_id
|
|
}
|
|
className={`px-4 py-2 rounded-md ${
|
|
!profile?.telegram_bot_token ||
|
|
!profile?.telegram_chat_id
|
|
? 'bg-gray-300 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed'
|
|
: 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600'
|
|
}`}
|
|
onClick={async () => {
|
|
try {
|
|
const response = await fetch(
|
|
'/api/profile/task-summary/send-now',
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type':
|
|
'application/json',
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const data =
|
|
await response.json();
|
|
throw new Error(
|
|
data.error ||
|
|
t(
|
|
'profile.sendSummaryFailed'
|
|
)
|
|
);
|
|
}
|
|
|
|
const data = await response.json();
|
|
showSuccessToast(data.message);
|
|
} catch (error) {
|
|
showErrorToast(
|
|
(error as Error).message
|
|
);
|
|
}
|
|
}}
|
|
>
|
|
{t(
|
|
'profile.sendTestSummary',
|
|
'Send Test Summary'
|
|
)}
|
|
</button>
|
|
{(!profile?.telegram_bot_token ||
|
|
!profile?.telegram_chat_id) && (
|
|
<p className="mt-2 text-xs text-red-500">
|
|
{t(
|
|
'profile.telegramRequiredForSummaries',
|
|
'Telegram integration must be set up to use task summaries.'
|
|
)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* AI Features Tab */}
|
|
{activeTab === 'ai' && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700">
|
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-6 flex items-center">
|
|
<LightBulbIcon className="w-6 h-6 mr-3 text-blue-500" />
|
|
{t(
|
|
'profile.aiProductivityFeatures',
|
|
'AI & Productivity Features'
|
|
)}
|
|
</h3>
|
|
|
|
{/* Task Intelligence Subsection */}
|
|
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-3 flex items-center">
|
|
<BoltIcon className="w-5 h-5 mr-2 text-purple-500" />
|
|
{t(
|
|
'profile.taskIntelligence',
|
|
'Task Intelligence'
|
|
)}
|
|
</h4>
|
|
|
|
<div className="mb-4 text-sm text-gray-600 dark:text-gray-300 flex items-start">
|
|
<InformationCircleIcon className="h-5 w-5 mr-2 flex-shrink-0 text-blue-500" />
|
|
<p>
|
|
{t(
|
|
'profile.taskIntelligenceDescription',
|
|
'Get helpful suggestions to make your task names more descriptive and actionable.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{t(
|
|
'profile.enableTaskIntelligence',
|
|
'Enable Task Intelligence Assistant'
|
|
)}
|
|
</label>
|
|
<div
|
|
className={`relative inline-block w-12 h-6 transition-colors duration-200 ease-in-out rounded-full cursor-pointer ${
|
|
formData.task_intelligence_enabled
|
|
? 'bg-blue-500'
|
|
: 'bg-gray-300 dark:bg-gray-600'
|
|
}`}
|
|
onClick={() => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
task_intelligence_enabled:
|
|
!prev.task_intelligence_enabled,
|
|
}));
|
|
}}
|
|
>
|
|
<span
|
|
className={`absolute left-0 top-0 bottom-0 m-1 w-4 h-4 transition-transform duration-200 ease-in-out transform bg-white rounded-full ${
|
|
formData.task_intelligence_enabled
|
|
? 'translate-x-6'
|
|
: 'translate-x-0'
|
|
}`}
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Auto-Suggest Next Actions Subsection */}
|
|
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mt-4">
|
|
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-3 flex items-center">
|
|
<ChevronRightIcon className="w-5 h-5 mr-2 text-green-500" />
|
|
{t(
|
|
'profile.autoSuggestNextActions',
|
|
'Auto-Suggest Next Actions'
|
|
)}
|
|
</h4>
|
|
|
|
<div className="mb-4 text-sm text-gray-600 dark:text-gray-300 flex items-start">
|
|
<InformationCircleIcon className="h-5 w-5 mr-2 flex-shrink-0 text-blue-500" />
|
|
<p>
|
|
{t(
|
|
'profile.autoSuggestNextActionsDescription',
|
|
'When creating a project, automatically prompt for the very next physical action to take.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{t(
|
|
'profile.enableAutoSuggestNextActions',
|
|
'Enable Next Action Prompts'
|
|
)}
|
|
</label>
|
|
<div
|
|
className={`relative inline-block w-12 h-6 transition-colors duration-200 ease-in-out rounded-full cursor-pointer ${
|
|
formData.auto_suggest_next_actions_enabled
|
|
? 'bg-blue-500'
|
|
: 'bg-gray-300 dark:bg-gray-600'
|
|
}`}
|
|
onClick={() => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
auto_suggest_next_actions_enabled:
|
|
!prev.auto_suggest_next_actions_enabled,
|
|
}));
|
|
}}
|
|
>
|
|
<span
|
|
className={`absolute left-0 top-0 bottom-0 m-1 w-4 h-4 transition-transform duration-200 ease-in-out transform bg-white rounded-full ${
|
|
formData.auto_suggest_next_actions_enabled
|
|
? 'translate-x-6'
|
|
: 'translate-x-0'
|
|
}`}
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Productivity Assistant Subsection */}
|
|
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mt-4">
|
|
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-3 flex items-center">
|
|
<ExclamationTriangleIcon className="w-5 h-5 mr-2 text-yellow-500" />
|
|
{t(
|
|
'profile.productivityAssistant',
|
|
'Productivity Assistant'
|
|
)}
|
|
</h4>
|
|
|
|
<div className="mb-4 text-sm text-gray-600 dark:text-gray-300 flex items-start">
|
|
<InformationCircleIcon className="h-5 w-5 mr-2 flex-shrink-0 text-blue-500" />
|
|
<p>
|
|
{t(
|
|
'profile.productivityAssistantDescription',
|
|
'Show productivity insights that help identify stalled projects, vague tasks, and workflow improvements on your Today page.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{t(
|
|
'profile.enableProductivityAssistant',
|
|
'Enable Productivity Insights'
|
|
)}
|
|
</label>
|
|
<div
|
|
className={`relative inline-block w-12 h-6 transition-colors duration-200 ease-in-out rounded-full cursor-pointer ${
|
|
formData.productivity_assistant_enabled
|
|
? 'bg-blue-500'
|
|
: 'bg-gray-300 dark:bg-gray-600'
|
|
}`}
|
|
onClick={() => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
productivity_assistant_enabled:
|
|
!prev.productivity_assistant_enabled,
|
|
}));
|
|
}}
|
|
>
|
|
<span
|
|
className={`absolute left-0 top-0 bottom-0 m-1 w-4 h-4 transition-transform duration-200 ease-in-out transform bg-white rounded-full ${
|
|
formData.productivity_assistant_enabled
|
|
? 'translate-x-6'
|
|
: 'translate-x-0'
|
|
}`}
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Next Task Suggestion Subsection */}
|
|
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mt-4">
|
|
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-3 flex items-center">
|
|
<FaceSmileIcon className="w-5 h-5 mr-2 text-green-500" />
|
|
{t(
|
|
'profile.nextTaskSuggestion',
|
|
'Next Task Suggestion'
|
|
)}
|
|
</h4>
|
|
|
|
<div className="mb-4 text-sm text-gray-600 dark:text-gray-300 flex items-start">
|
|
<InformationCircleIcon className="h-5 w-5 mr-2 flex-shrink-0 text-blue-500" />
|
|
<p>
|
|
{t(
|
|
'profile.nextTaskSuggestionDescription',
|
|
'Automatically suggest the next best task to work on when you have nothing in progress, prioritizing due today tasks, then suggested tasks, then next actions.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{t(
|
|
'profile.enableNextTaskSuggestion',
|
|
'Enable Next Task Suggestions'
|
|
)}
|
|
</label>
|
|
<div
|
|
className={`relative inline-block w-12 h-6 transition-colors duration-200 ease-in-out rounded-full cursor-pointer ${
|
|
formData.next_task_suggestion_enabled
|
|
? 'bg-blue-500'
|
|
: 'bg-gray-300 dark:bg-gray-600'
|
|
}`}
|
|
onClick={() => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
next_task_suggestion_enabled:
|
|
!prev.next_task_suggestion_enabled,
|
|
}));
|
|
}}
|
|
>
|
|
<span
|
|
className={`absolute left-0 top-0 bottom-0 m-1 w-4 h-4 transition-transform duration-200 ease-in-out transform bg-white rounded-full ${
|
|
formData.next_task_suggestion_enabled
|
|
? 'translate-x-6'
|
|
: 'translate-x-0'
|
|
}`}
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Save Button */}
|
|
<div className="flex justify-end dark:border-gray-700">
|
|
<button
|
|
type="submit"
|
|
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 transition-colors duration-200 flex items-center space-x-2"
|
|
>
|
|
<CheckIcon className="w-5 h-5" />
|
|
<span>{t('profile.saveChanges', 'Save Changes')}</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProfileSettings;
|