2025-01-20 08:53:15 +00:00
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import { motion } from 'framer-motion';
|
|
|
|
|
import { toast } from 'react-toastify';
|
|
|
|
|
import { classNames } from '~/utils/classNames';
|
|
|
|
|
import { Switch } from '~/components/ui/Switch';
|
|
|
|
|
import { themeStore, kTheme } from '~/lib/stores/theme';
|
2025-02-02 00:42:30 +00:00
|
|
|
|
import type { UserProfile } from '~/components/@settings/core/types';
|
2025-01-24 00:08:51 +00:00
|
|
|
|
import { useStore } from '@nanostores/react';
|
|
|
|
|
import { shortcutsStore } from '~/lib/stores/settings';
|
2025-01-20 08:53:15 +00:00
|
|
|
|
|
|
|
|
|
export default function SettingsTab() {
|
|
|
|
|
const [currentTimezone, setCurrentTimezone] = useState('');
|
|
|
|
|
const [settings, setSettings] = useState<UserProfile>(() => {
|
|
|
|
|
const saved = localStorage.getItem('bolt_user_profile');
|
|
|
|
|
return saved
|
|
|
|
|
? JSON.parse(saved)
|
|
|
|
|
: {
|
|
|
|
|
theme: 'system',
|
|
|
|
|
notifications: true,
|
|
|
|
|
language: 'en',
|
|
|
|
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setCurrentTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Apply theme when settings changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (settings.theme === 'system') {
|
|
|
|
|
// Remove theme override
|
|
|
|
|
localStorage.removeItem(kTheme);
|
|
|
|
|
|
|
|
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
|
|
|
document.querySelector('html')?.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
2025-01-30 00:58:47 +00:00
|
|
|
|
themeStore.set(prefersDark ? 'dark' : 'light');
|
2025-01-20 08:53:15 +00:00
|
|
|
|
} else {
|
2025-01-30 00:58:47 +00:00
|
|
|
|
themeStore.set(settings.theme);
|
2025-01-20 08:53:15 +00:00
|
|
|
|
localStorage.setItem(kTheme, settings.theme);
|
|
|
|
|
document.querySelector('html')?.setAttribute('data-theme', settings.theme);
|
|
|
|
|
}
|
|
|
|
|
}, [settings.theme]);
|
|
|
|
|
|
|
|
|
|
// Save settings automatically when they change
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
try {
|
|
|
|
|
// Get existing profile data
|
|
|
|
|
const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
|
|
|
|
|
|
|
|
|
|
// Merge with new settings
|
|
|
|
|
const updatedProfile = {
|
|
|
|
|
...existingProfile,
|
|
|
|
|
theme: settings.theme,
|
|
|
|
|
notifications: settings.notifications,
|
|
|
|
|
language: settings.language,
|
|
|
|
|
timezone: settings.timezone,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
|
|
|
|
|
toast.success('Settings updated');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error saving settings:', error);
|
|
|
|
|
toast.error('Failed to update settings');
|
|
|
|
|
}
|
|
|
|
|
}, [settings]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{/* Theme & Language */}
|
|
|
|
|
<motion.div
|
|
|
|
|
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4 space-y-4"
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ delay: 0.1 }}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2 mb-4">
|
|
|
|
|
<div className="i-ph:palette-fill w-4 h-4 text-purple-500" />
|
|
|
|
|
<span className="text-sm font-medium text-bolt-elements-textPrimary">Appearance</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
|
<div className="i-ph:paint-brush-fill w-4 h-4 text-bolt-elements-textSecondary" />
|
|
|
|
|
<label className="block text-sm text-bolt-elements-textSecondary">Theme</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
{(['light', 'dark', 'system'] as const).map((theme) => (
|
|
|
|
|
<button
|
|
|
|
|
key={theme}
|
2025-01-30 00:58:47 +00:00
|
|
|
|
onClick={() => {
|
|
|
|
|
setSettings((prev) => ({ ...prev, theme }));
|
|
|
|
|
|
|
|
|
|
if (theme !== 'system') {
|
|
|
|
|
themeStore.set(theme);
|
|
|
|
|
}
|
|
|
|
|
}}
|
2025-01-20 08:53:15 +00:00
|
|
|
|
className={classNames(
|
2025-01-30 16:17:36 +00:00
|
|
|
|
'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed',
|
2025-01-20 08:53:15 +00:00
|
|
|
|
settings.theme === theme
|
2025-01-30 16:17:36 +00:00
|
|
|
|
? 'bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-500 dark:text-white dark:hover:bg-purple-600'
|
|
|
|
|
: 'bg-bolt-elements-hover dark:bg-[#1A1A1A] text-bolt-elements-textSecondary hover:bg-purple-500/10 hover:text-purple-500 dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
2025-01-20 08:53:15 +00:00
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className={`w-4 h-4 ${
|
|
|
|
|
theme === 'light'
|
|
|
|
|
? 'i-ph:sun-fill'
|
|
|
|
|
: theme === 'dark'
|
|
|
|
|
? 'i-ph:moon-stars-fill'
|
|
|
|
|
: 'i-ph:monitor-fill'
|
|
|
|
|
}`}
|
|
|
|
|
/>
|
|
|
|
|
<span className="capitalize">{theme}</span>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
|
<div className="i-ph:translate-fill w-4 h-4 text-bolt-elements-textSecondary" />
|
|
|
|
|
<label className="block text-sm text-bolt-elements-textSecondary">Language</label>
|
|
|
|
|
</div>
|
|
|
|
|
<select
|
|
|
|
|
value={settings.language}
|
|
|
|
|
onChange={(e) => setSettings((prev) => ({ ...prev, language: e.target.value }))}
|
|
|
|
|
className={classNames(
|
|
|
|
|
'w-full px-3 py-2 rounded-lg text-sm',
|
2025-01-22 14:25:55 +00:00
|
|
|
|
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
2025-01-20 08:53:15 +00:00
|
|
|
|
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
|
|
|
|
'text-bolt-elements-textPrimary',
|
|
|
|
|
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
|
|
|
|
'transition-all duration-200',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<option value="en">English</option>
|
|
|
|
|
<option value="es">Español</option>
|
|
|
|
|
<option value="fr">Français</option>
|
|
|
|
|
<option value="de">Deutsch</option>
|
|
|
|
|
<option value="it">Italiano</option>
|
|
|
|
|
<option value="pt">Português</option>
|
|
|
|
|
<option value="ru">Русский</option>
|
|
|
|
|
<option value="zh">中文</option>
|
|
|
|
|
<option value="ja">日本語</option>
|
|
|
|
|
<option value="ko">한국어</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
|
<div className="i-ph:bell-fill w-4 h-4 text-bolt-elements-textSecondary" />
|
|
|
|
|
<label className="block text-sm text-bolt-elements-textSecondary">Notifications</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-sm text-bolt-elements-textSecondary">
|
|
|
|
|
{settings.notifications ? 'Notifications are enabled' : 'Notifications are disabled'}
|
|
|
|
|
</span>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={settings.notifications}
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
// Update local state
|
|
|
|
|
setSettings((prev) => ({ ...prev, notifications: checked }));
|
|
|
|
|
|
|
|
|
|
// Update localStorage immediately
|
|
|
|
|
const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
|
|
|
|
|
const updatedProfile = {
|
|
|
|
|
...existingProfile,
|
|
|
|
|
notifications: checked,
|
|
|
|
|
};
|
|
|
|
|
localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
|
|
|
|
|
|
|
|
|
|
// Dispatch storage event for other components
|
|
|
|
|
window.dispatchEvent(
|
|
|
|
|
new StorageEvent('storage', {
|
|
|
|
|
key: 'bolt_user_profile',
|
|
|
|
|
newValue: JSON.stringify(updatedProfile),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
toast.success(`Notifications ${checked ? 'enabled' : 'disabled'}`);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
{/* Timezone */}
|
|
|
|
|
<motion.div
|
|
|
|
|
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4"
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ delay: 0.2 }}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2 mb-4">
|
|
|
|
|
<div className="i-ph:clock-fill w-4 h-4 text-purple-500" />
|
|
|
|
|
<span className="text-sm font-medium text-bolt-elements-textPrimary">Time Settings</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
|
<div className="i-ph:globe-fill w-4 h-4 text-bolt-elements-textSecondary" />
|
|
|
|
|
<label className="block text-sm text-bolt-elements-textSecondary">Timezone</label>
|
|
|
|
|
</div>
|
|
|
|
|
<select
|
|
|
|
|
value={settings.timezone}
|
|
|
|
|
onChange={(e) => setSettings((prev) => ({ ...prev, timezone: e.target.value }))}
|
|
|
|
|
className={classNames(
|
|
|
|
|
'w-full px-3 py-2 rounded-lg text-sm',
|
2025-01-22 14:25:55 +00:00
|
|
|
|
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
2025-01-20 08:53:15 +00:00
|
|
|
|
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
|
|
|
|
'text-bolt-elements-textPrimary',
|
|
|
|
|
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
|
|
|
|
'transition-all duration-200',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<option value={currentTimezone}>{currentTimezone}</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
2025-01-24 00:08:51 +00:00
|
|
|
|
|
|
|
|
|
{/* Keyboard Shortcuts */}
|
|
|
|
|
<motion.div
|
|
|
|
|
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4"
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ delay: 0.3 }}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2 mb-4">
|
|
|
|
|
<div className="i-ph:keyboard-fill w-4 h-4 text-purple-500" />
|
|
|
|
|
<span className="text-sm font-medium text-bolt-elements-textPrimary">Keyboard Shortcuts</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{Object.entries(useStore(shortcutsStore)).map(([name, shortcut]) => (
|
2025-01-28 21:57:06 +00:00
|
|
|
|
<div
|
|
|
|
|
key={name}
|
|
|
|
|
className="flex items-center justify-between p-2 rounded-lg bg-[#FAFAFA] dark:bg-[#1A1A1A] hover:bg-purple-50 dark:hover:bg-purple-500/10 transition-colors"
|
|
|
|
|
>
|
2025-01-24 00:08:51 +00:00
|
|
|
|
<span className="text-sm text-bolt-elements-textPrimary capitalize">
|
|
|
|
|
{name.replace(/([A-Z])/g, ' $1').toLowerCase()}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
{shortcut.ctrlOrMetaKey && (
|
2025-01-28 21:57:06 +00:00
|
|
|
|
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
|
|
|
|
{navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}
|
|
|
|
|
</kbd>
|
2025-01-24 00:08:51 +00:00
|
|
|
|
)}
|
2025-01-28 21:57:06 +00:00
|
|
|
|
{shortcut.ctrlKey && (
|
|
|
|
|
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
|
|
|
|
Ctrl
|
|
|
|
|
</kbd>
|
|
|
|
|
)}
|
|
|
|
|
{shortcut.metaKey && (
|
|
|
|
|
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
|
|
|
|
⌘
|
|
|
|
|
</kbd>
|
|
|
|
|
)}
|
|
|
|
|
{shortcut.altKey && (
|
|
|
|
|
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
|
|
|
|
{navigator.platform.includes('Mac') ? '⌥' : 'Alt'}
|
|
|
|
|
</kbd>
|
|
|
|
|
)}
|
|
|
|
|
{shortcut.shiftKey && (
|
|
|
|
|
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
|
|
|
|
⇧
|
|
|
|
|
</kbd>
|
|
|
|
|
)}
|
|
|
|
|
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
|
|
|
|
{shortcut.key.toUpperCase()}
|
|
|
|
|
</kbd>
|
2025-01-24 00:08:51 +00:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
2025-01-20 08:53:15 +00:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|