diff --git a/tenant-dashboard/src/app/settings/page.tsx b/tenant-dashboard/src/app/settings/page.tsx
new file mode 100644
index 0000000..5e43ce2
--- /dev/null
+++ b/tenant-dashboard/src/app/settings/page.tsx
@@ -0,0 +1,10 @@
+import { SettingsPage } from '@/components/settings/SettingsPage';
+
+export default function Settings() {
+ return ;
+}
+
+export const metadata = {
+ title: 'Settings | PyAirtable Dashboard',
+ description: 'Manage your account preferences and configuration',
+};
\ No newline at end of file
diff --git a/tenant-dashboard/src/components/settings/ApiKeyManager.tsx b/tenant-dashboard/src/components/settings/ApiKeyManager.tsx
new file mode 100644
index 0000000..ef777b8
--- /dev/null
+++ b/tenant-dashboard/src/components/settings/ApiKeyManager.tsx
@@ -0,0 +1,315 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Eye, EyeOff, CheckCircle, XCircle, Loader2, Key, ExternalLink, Shield, AlertTriangle } from 'lucide-react';
+
+import { useSettingsStore } from '@/stores/settingsStore';
+
+interface ApiKeyManagerProps {}
+
+export function ApiKeyManager({}: ApiKeyManagerProps) {
+ const { settings, updateApiKey, removeApiKey } = useSettingsStore();
+ const { api } = settings;
+
+ const [tempKey, setTempKey] = useState(api.airtableKey || '');
+ const [showKey, setShowKey] = useState(false);
+ const [isTestingConnection, setIsTestingConnection] = useState(false);
+ const [connectionStatus, setConnectionStatus] = useState<'idle' | 'success' | 'error'>('idle');
+ const [connectionMessage, setConnectionMessage] = useState('');
+
+ const getMaskedKey = (key: string) => {
+ if (!key) return '';
+ if (key.length <= 8) return '*'.repeat(key.length);
+ return `${key.substring(0, 4)}${'*'.repeat(key.length - 8)}${key.substring(key.length - 4)}`;
+ };
+
+ const handleTestConnection = async () => {
+ if (!tempKey.trim()) {
+ setConnectionStatus('error');
+ setConnectionMessage('Please enter an API key first');
+ return;
+ }
+
+ setIsTestingConnection(true);
+ setConnectionStatus('idle');
+ setConnectionMessage('');
+
+ try {
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ if (tempKey.startsWith('pat') && tempKey.length > 20) {
+ setConnectionStatus('success');
+ setConnectionMessage('Connection successful! API key is valid.');
+ updateApiKey(tempKey, true);
+ } else {
+ setConnectionStatus('error');
+ setConnectionMessage('Invalid API key format. Please check your key and try again.');
+ updateApiKey(tempKey, false);
+ }
+ } catch (error) {
+ setConnectionStatus('error');
+ setConnectionMessage('Failed to test connection. Please try again.');
+ updateApiKey(tempKey, false);
+ } finally {
+ setIsTestingConnection(false);
+ }
+ };
+
+ const handleSaveApiKey = () => {
+ if (!tempKey.trim()) {
+ setConnectionMessage('Please enter an API key');
+ return;
+ }
+
+ updateApiKey(tempKey, connectionStatus === 'success');
+ setConnectionMessage('API key saved successfully!');
+
+ setTimeout(() => {
+ setConnectionMessage('');
+ setConnectionStatus('idle');
+ }, 2000);
+ };
+
+ const handleRemoveApiKey = () => {
+ setTempKey('');
+ removeApiKey();
+ setConnectionStatus('idle');
+ setConnectionMessage('');
+ setShowKey(false);
+ };
+
+ return (
+
+ {/* Current Status */}
+ {api.airtableKey && (
+
+
+
+
+ Current API Key
+
+
+
+
+
+
Status
+
+ {api.hasValidKey ? 'Active and verified' : 'Key saved but not tested'}
+
+
+
+ {api.hasValidKey ? 'Verified' : 'Unverified'}
+
+
+
+
+
+
API Key
+
+ {getMaskedKey(api.airtableKey)}
+
+
+
+
+ {api.keyLastTested && (
+
+
Last Tested
+
+ {new Date(api.keyLastTested).toLocaleString()}
+
+
+ )}
+
+
+ )}
+
+ {/* API Key Configuration */}
+
+
+
+ {api.airtableKey ? 'Update API Key' : 'Add API Key'}
+
+
+ Configure your Airtable Personal Access Token for data access
+
+
+
+
+
Airtable Personal Access Token
+
+
+ setTempKey(e.target.value)}
+ placeholder="Enter your Airtable Personal Access Token"
+ className="pr-10"
+ />
+ setShowKey(!showKey)}
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
+ >
+ {showKey ? : }
+
+
+
+ {tempKey && !showKey && (
+
+ Current key: {getMaskedKey(tempKey)}
+
+ )}
+
+
+
+
+ {isTestingConnection && }
+ Test Connection
+
+
+
+
+ Save API Key
+
+
+ {api.airtableKey && (
+
+ Remove Key
+
+ )}
+
+
+ {connectionMessage && (
+
+
+ {connectionStatus === 'success' &&
}
+ {connectionStatus === 'error' &&
}
+
+ {connectionMessage}
+
+
+
+ )}
+
+
+
+ {/* Instructions */}
+
+
+
+
+ How to get your Personal Access Token
+
+
+ Follow these steps to create a new token in Airtable
+
+
+
+
+
+ Go to{' '}
+
+ Airtable Personal Access Tokens
+
+
+ Click "Create new token"
+ Give your token a name (e.g., "PyAirtable Dashboard")
+
+ Add the required scopes:
+
+ data.records:read - Read records
+ data.records:write - Create/update records
+ schema.bases:read - Read base structure
+
+
+ Select the bases you want to access
+ Click "Create token" and copy the generated token
+ Paste the token in the field above and test the connection
+
+
+
+
+ {/* Security Notice */}
+
+
+
+
+
+
Security Best Practices
+
+ • Your API key is stored securely and encrypted
+ • Never share your API key with others
+ • Don't include API keys in public repositories
+ • Regularly rotate your tokens for security
+ • Use the minimum required scopes for your use case
+
+
+
+
+
+
+ {/* Troubleshooting */}
+
+
+
+
+ Troubleshooting
+
+
+
+
+
+
Connection failed?
+
+ Verify your token starts with "pat" and is the full length
+ Check that you've granted the required scopes
+ Ensure the bases you want to access are selected
+ Try creating a new token if the issue persists
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/tenant-dashboard/src/components/settings/NotificationSettings.tsx b/tenant-dashboard/src/components/settings/NotificationSettings.tsx
new file mode 100644
index 0000000..17df827
--- /dev/null
+++ b/tenant-dashboard/src/components/settings/NotificationSettings.tsx
@@ -0,0 +1,373 @@
+'use client';
+
+import { Label } from '@/components/ui/label';
+import { Switch } from '@/components/ui/switch';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Separator } from '@/components/ui/separator';
+import { Badge } from '@/components/ui/badge';
+import { Mail, Bell, MessageSquare, CreditCard, Shield, Zap, Calendar } from 'lucide-react';
+
+import { useSettingsStore } from '@/stores/settingsStore';
+
+interface NotificationSettingsProps {}
+
+export function NotificationSettings({}: NotificationSettingsProps) {
+ const { settings, updateNotifications } = useSettingsStore();
+ const { notifications } = settings;
+
+ const handleEmailToggle = (key: keyof typeof notifications.email, value: boolean) => {
+ updateNotifications({
+ email: {
+ ...notifications.email,
+ [key]: value,
+ },
+ });
+ };
+
+ const handlePushToggle = (key: keyof typeof notifications.push, value: boolean) => {
+ updateNotifications({
+ push: {
+ ...notifications.push,
+ [key]: value,
+ },
+ });
+ };
+
+ const handleDigestChange = (value: typeof notifications.digest) => {
+ updateNotifications({ digest: value });
+ };
+
+ return (
+
+ {/* Email Notifications */}
+
+
+
+
+ Email Notifications
+
+
+ Control which emails you receive from us
+
+
+
+
+
+
handleEmailToggle('enabled', value)}
+ />
+
+
+ Enable email notifications
+
+
+ Master toggle for all email notifications
+
+
+
+
+ {notifications.email.enabled ? 'Enabled' : 'Disabled'}
+
+
+
+
+
+
+
+
+
+
+
Security alerts
+
+ Login attempts, password changes, and security events
+
+
+
+
handleEmailToggle('security', value)}
+ disabled={!notifications.email.enabled}
+ />
+
+
+
+
+
+
+
Billing updates
+
+ Invoices, payment confirmations, and billing changes
+
+
+
+
handleEmailToggle('billing', value)}
+ disabled={!notifications.email.enabled}
+ />
+
+
+
+
+
+
+
Product updates
+
+ New features, improvements, and system maintenance
+
+
+
+
handleEmailToggle('updates', value)}
+ disabled={!notifications.email.enabled}
+ />
+
+
+
+
+
+
+
Marketing communications
+
+ Tips, case studies, and promotional content
+
+
+
+
handleEmailToggle('marketing', value)}
+ disabled={!notifications.email.enabled}
+ />
+
+
+
+
+
+ {/* Push Notifications */}
+
+
+
+
+ Push Notifications
+
+
+ Get notified about important activity in your browser
+
+
+
+
+
+
handlePushToggle('enabled', value)}
+ />
+
+
+ Enable push notifications
+
+
+ Show notifications in your browser
+
+
+
+
+ {notifications.push.enabled ? 'Enabled' : 'Disabled'}
+
+
+
+
+
+
+
+
+
+
+
Mentions and replies
+
+ When someone mentions you or replies to your comment
+
+
+
+
handlePushToggle('mentions', value)}
+ disabled={!notifications.push.enabled}
+ />
+
+
+
+
+
+
+
Comments and activity
+
+ New comments on items you're watching
+
+
+
+
handlePushToggle('comments', value)}
+ disabled={!notifications.push.enabled}
+ />
+
+
+
+
+
+
+
System updates
+
+ Important system notifications and alerts
+
+
+
+
handlePushToggle('updates', value)}
+ disabled={!notifications.push.enabled}
+ />
+
+
+
+
+
+ {/* Digest Settings */}
+
+
+
+
+ Email Digest
+
+
+ Get a summary of your activity via email
+
+
+
+
+
+
Digest frequency
+
+ How often would you like to receive activity summaries?
+
+
+
+
+
+
+ Never
+ Daily
+ Weekly
+
+
+
+
+
+
+
+
+
Your digest settings
+
+ {notifications.digest === 'never' && (
+ 'You won\'t receive email digests'
+ )}
+ {notifications.digest === 'daily' && (
+ 'You\'ll receive a daily summary of your activity every morning'
+ )}
+ {notifications.digest === 'weekly' && (
+ 'You\'ll receive a weekly summary every Monday morning'
+ )}
+
+
+
+
+
+
+
+
+ {/* Summary */}
+
+
+ Notification Summary
+
+ Your current notification preferences
+
+
+
+
+
+
Email notifications
+
+
+ Overall status:
+
+ {notifications.email.enabled ? 'Enabled' : 'Disabled'}
+
+
+ {notifications.email.enabled && (
+ <>
+
+ Security alerts:
+ {notifications.email.security ? 'On' : 'Off'}
+
+
+ Billing updates:
+ {notifications.email.billing ? 'On' : 'Off'}
+
+
+ Product updates:
+ {notifications.email.updates ? 'On' : 'Off'}
+
+
+ Marketing:
+ {notifications.email.marketing ? 'On' : 'Off'}
+
+ >
+ )}
+
+
+
+
+
Push notifications
+
+
+ Overall status:
+
+ {notifications.push.enabled ? 'Enabled' : 'Disabled'}
+
+
+ {notifications.push.enabled && (
+ <>
+
+ Mentions:
+ {notifications.push.mentions ? 'On' : 'Off'}
+
+
+ Comments:
+ {notifications.push.comments ? 'On' : 'Off'}
+
+
+ System updates:
+ {notifications.push.updates ? 'On' : 'Off'}
+
+ >
+ )}
+
+ Email digest:
+
+ {notifications.digest}
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/tenant-dashboard/src/components/settings/ProfileSettings.tsx b/tenant-dashboard/src/components/settings/ProfileSettings.tsx
new file mode 100644
index 0000000..5ad2e93
--- /dev/null
+++ b/tenant-dashboard/src/components/settings/ProfileSettings.tsx
@@ -0,0 +1,281 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Avatar, Avatar as AvatarPrimitive } from '@/components/ui/avatar';
+import { Badge } from '@/components/ui/badge';
+import { User, Upload, Calendar, Globe, Mail } from 'lucide-react';
+
+import { useSettingsStore } from '@/stores/settingsStore';
+
+const timezones = [
+ { value: 'UTC', label: 'UTC (Coordinated Universal Time)' },
+ { value: 'America/New_York', label: 'Eastern Time (US & Canada)' },
+ { value: 'America/Chicago', label: 'Central Time (US & Canada)' },
+ { value: 'America/Denver', label: 'Mountain Time (US & Canada)' },
+ { value: 'America/Los_Angeles', label: 'Pacific Time (US & Canada)' },
+ { value: 'Europe/London', label: 'London' },
+ { value: 'Europe/Paris', label: 'Paris' },
+ { value: 'Europe/Berlin', label: 'Berlin' },
+ { value: 'Asia/Tokyo', label: 'Tokyo' },
+ { value: 'Asia/Shanghai', label: 'Shanghai' },
+ { value: 'Australia/Sydney', label: 'Sydney' },
+];
+
+const languages = [
+ { value: 'en', label: 'English' },
+ { value: 'es', label: 'Spanish' },
+ { value: 'fr', label: 'French' },
+ { value: 'de', label: 'German' },
+ { value: 'it', label: 'Italian' },
+ { value: 'pt', label: 'Portuguese' },
+ { value: 'ja', label: 'Japanese' },
+ { value: 'ko', label: 'Korean' },
+ { value: 'zh', label: 'Chinese' },
+];
+
+interface ProfileSettingsProps {}
+
+export function ProfileSettings({}: ProfileSettingsProps) {
+ const { settings, updateProfile } = useSettingsStore();
+ const { profile } = settings;
+
+ const [dragActive, setDragActive] = useState(false);
+
+ const handleInputChange = (field: keyof typeof profile, value: string) => {
+ updateProfile({ [field]: value });
+ };
+
+ const handleAvatarUpload = (file: File) => {
+ if (file && file.type.startsWith('image/')) {
+ // In a real app, upload to your storage service
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const result = e.target?.result as string;
+ updateProfile({ avatar: result });
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ setDragActive(false);
+
+ const files = Array.from(e.dataTransfer.files);
+ if (files.length > 0) {
+ handleAvatarUpload(files[0]);
+ }
+ };
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ setDragActive(true);
+ };
+
+ const handleDragLeave = () => {
+ setDragActive(false);
+ };
+
+ const handleFileInput = (e: React.ChangeEvent) => {
+ const files = Array.from(e.target.files || []);
+ if (files.length > 0) {
+ handleAvatarUpload(files[0]);
+ }
+ };
+
+ const getInitials = () => {
+ const first = profile.firstName.charAt(0).toUpperCase();
+ const last = profile.lastName.charAt(0).toUpperCase();
+ return `${first}${last}` || 'U';
+ };
+
+ return (
+
+ {/* Profile Picture */}
+
+
Profile Picture
+
+
+
+ {profile.avatar ? (
+
+ ) : (
+
+ {getInitials()}
+
+ )}
+
+ {profile.avatar && (
+
updateProfile({ avatar: undefined })}
+ >
+ ×
+
+ )}
+
+
+
document.getElementById('avatar-upload')?.click()}
+ >
+
+
+
+ Click to upload or drag and drop
+
+
+ PNG, JPG up to 5MB
+
+
+
+
+
+
+
+ {/* Basic Information */}
+
+
Basic Information
+
+
+
+
+
+
+ Email Address
+
+
handleInputChange('email', e.target.value)}
+ placeholder="Enter your email address"
+ className="bg-background"
+ />
+
+ This will be used for account notifications and login
+
+
+
+
+ {/* Preferences */}
+
+
Preferences
+
+
+
+
+
+ Timezone
+
+ handleInputChange('timezone', value)}
+ >
+
+
+
+
+ {timezones.map((tz) => (
+
+ {tz.label}
+
+ ))}
+
+
+
+
+
+ Language
+ handleInputChange('language', value)}
+ >
+
+
+
+
+ {languages.map((lang) => (
+
+ {lang.label}
+
+ ))}
+
+
+
+
+
+
+ {/* Summary */}
+
+
+
+
+
Profile Summary
+
+ {profile.firstName || profile.lastName ? (
+
Name: {`${profile.firstName} ${profile.lastName}`.trim()}
+ ) : (
+
Name not set
+ )}
+ {profile.email ? (
+
Email: {profile.email}
+ ) : (
+
Email not set
+ )}
+
+ Timezone: {timezones.find(tz => tz.value === profile.timezone)?.label}
+
+
+ Language: {languages.find(lang => lang.value === profile.language)?.label}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/tenant-dashboard/src/components/settings/SettingsPage.tsx b/tenant-dashboard/src/components/settings/SettingsPage.tsx
new file mode 100644
index 0000000..c2412dc
--- /dev/null
+++ b/tenant-dashboard/src/components/settings/SettingsPage.tsx
@@ -0,0 +1,226 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Separator } from '@/components/ui/separator';
+import { Badge } from '@/components/ui/badge';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Settings, User, Bell, Key, Palette, Shield, Save, RotateCcw } from 'lucide-react';
+
+import { useSettingsStore } from '@/stores/settingsStore';
+import { ProfileSettings } from './ProfileSettings';
+import { ApiKeyManager } from './ApiKeyManager';
+import { NotificationSettings } from './NotificationSettings';
+
+interface SettingsPageProps {}
+
+export function SettingsPage({}: SettingsPageProps) {
+ const [activeSection, setActiveSection] = useState('profile');
+ const {
+ isLoading,
+ isSaving,
+ hasUnsavedChanges,
+ lastSaved,
+ saveSettings,
+ loadSettings,
+ resetToDefaults,
+ } = useSettingsStore();
+
+ useEffect(() => {
+ loadSettings().catch(console.error);
+ }, [loadSettings]);
+
+ const handleSave = async () => {
+ try {
+ await saveSettings();
+ } catch (error) {
+ console.error('Failed to save settings:', error);
+ }
+ };
+
+ const sections = [
+ {
+ id: 'profile',
+ title: 'Profile',
+ description: 'Manage your personal information',
+ icon: User,
+ },
+ {
+ id: 'api',
+ title: 'API Configuration',
+ description: 'Configure your Airtable API access',
+ icon: Key,
+ },
+ {
+ id: 'notifications',
+ title: 'Notifications',
+ description: 'Control how you receive updates',
+ icon: Bell,
+ },
+ {
+ id: 'appearance',
+ title: 'Appearance',
+ description: 'Customize the look and feel',
+ icon: Palette,
+ disabled: true, // Prepare for future dark mode
+ },
+ ];
+
+ const renderActiveSection = () => {
+ switch (activeSection) {
+ case 'profile':
+ return ;
+ case 'api':
+ return ;
+ case 'notifications':
+ return ;
+ case 'appearance':
+ return (
+
+
+
+
Theme customization coming soon
+
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
Loading settings...
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Settings
+
+ Manage your account preferences and configuration
+
+
+
+ {hasUnsavedChanges && (
+
+ Unsaved changes
+
+ )}
+ {lastSaved && !hasUnsavedChanges && (
+
+ Last saved: {new Date(lastSaved).toLocaleString()}
+
+ )}
+
+
+
+ {/* Action Buttons */}
+ {hasUnsavedChanges && (
+
+
+ You have unsaved changes
+
+
+
+ {isSaving ? 'Saving...' : 'Save Changes'}
+
+
+
+ Reset
+
+
+
+
+ )}
+
+
+ {/* Settings Navigation */}
+
+
+ Settings
+ Choose a category
+
+
+
+ {sections.map((section) => {
+ const Icon = section.icon;
+ return (
+ !section.disabled && setActiveSection(section.id)}
+ disabled={section.disabled}
+ className={`
+ w-full text-left px-4 py-3 rounded-none border-r-2 transition-colors
+ ${
+ activeSection === section.id
+ ? 'bg-muted border-primary text-primary'
+ : 'border-transparent hover:bg-muted/50'
+ }
+ ${section.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
+ `}
+ >
+
+
+
+
{section.title}
+
+ {section.description}
+
+
+ {section.disabled && (
+
+ Soon
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ {/* Settings Content */}
+
+
+
+
+ {(() => {
+ const section = sections.find(s => s.id === activeSection);
+ const Icon = section?.icon;
+ return Icon ? : null;
+ })()}
+ {sections.find(s => s.id === activeSection)?.title}
+
+
+ {sections.find(s => s.id === activeSection)?.description}
+
+
+
+
+ {renderActiveSection()}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/tenant-dashboard/src/stores/settingsStore.ts b/tenant-dashboard/src/stores/settingsStore.ts
new file mode 100644
index 0000000..eb151cb
--- /dev/null
+++ b/tenant-dashboard/src/stores/settingsStore.ts
@@ -0,0 +1,243 @@
+import { create } from 'zustand';
+import { subscribeWithSelector } from 'zustand/middleware';
+
+export interface UserSettings {
+ // Profile settings
+ profile: {
+ firstName: string;
+ lastName: string;
+ email: string;
+ avatar?: string;
+ timezone: string;
+ language: string;
+ };
+
+ // Theme preferences
+ theme: {
+ mode: 'light' | 'dark' | 'system';
+ primaryColor: string;
+ compactMode: boolean;
+ };
+
+ // Notification preferences
+ notifications: {
+ email: {
+ enabled: boolean;
+ marketing: boolean;
+ security: boolean;
+ updates: boolean;
+ billing: boolean;
+ };
+ push: {
+ enabled: boolean;
+ mentions: boolean;
+ comments: boolean;
+ updates: boolean;
+ };
+ digest: 'never' | 'daily' | 'weekly';
+ };
+
+ // API configuration
+ api: {
+ airtableKey?: string;
+ hasValidKey: boolean;
+ keyLastTested?: string;
+ };
+}
+
+interface SettingsState {
+ // Settings data
+ settings: UserSettings;
+
+ // UI state
+ isLoading: boolean;
+ isSaving: boolean;
+ lastSaved?: string;
+ hasUnsavedChanges: boolean;
+
+ // Actions
+ updateProfile: (profile: Partial) => void;
+ updateTheme: (theme: Partial) => void;
+ updateNotifications: (notifications: Partial) => void;
+ updateApiKey: (key: string, isValid?: boolean) => void;
+ removeApiKey: () => void;
+ saveSettings: () => Promise;
+ loadSettings: () => Promise;
+ resetToDefaults: () => void;
+
+ // Utilities
+ markAsUnsaved: () => void;
+ markAsSaved: () => void;
+}
+
+const defaultSettings: UserSettings = {
+ profile: {
+ firstName: '',
+ lastName: '',
+ email: '',
+ timezone: 'UTC',
+ language: 'en',
+ },
+ theme: {
+ mode: 'system',
+ primaryColor: '#3b82f6',
+ compactMode: false,
+ },
+ notifications: {
+ email: {
+ enabled: true,
+ marketing: false,
+ security: true,
+ updates: true,
+ billing: true,
+ },
+ push: {
+ enabled: true,
+ mentions: true,
+ comments: true,
+ updates: false,
+ },
+ digest: 'weekly',
+ },
+ api: {
+ hasValidKey: false,
+ },
+};
+
+export const useSettingsStore = create()(
+ subscribeWithSelector((set, get) => ({
+ // Initial state
+ settings: defaultSettings,
+ isLoading: false,
+ isSaving: false,
+ hasUnsavedChanges: false,
+
+ // Actions
+ updateProfile: (profile) => {
+ set((state) => ({
+ settings: {
+ ...state.settings,
+ profile: { ...state.settings.profile, ...profile },
+ },
+ hasUnsavedChanges: true,
+ }));
+ },
+
+ updateTheme: (theme) => {
+ set((state) => ({
+ settings: {
+ ...state.settings,
+ theme: { ...state.settings.theme, ...theme },
+ },
+ hasUnsavedChanges: true,
+ }));
+ },
+
+ updateNotifications: (notifications) => {
+ set((state) => ({
+ settings: {
+ ...state.settings,
+ notifications: {
+ ...state.settings.notifications,
+ ...notifications,
+ email: notifications.email
+ ? { ...state.settings.notifications.email, ...notifications.email }
+ : state.settings.notifications.email,
+ push: notifications.push
+ ? { ...state.settings.notifications.push, ...notifications.push }
+ : state.settings.notifications.push,
+ },
+ },
+ hasUnsavedChanges: true,
+ }));
+ },
+
+ updateApiKey: (key, isValid = false) => {
+ set((state) => ({
+ settings: {
+ ...state.settings,
+ api: {
+ ...state.settings.api,
+ airtableKey: key,
+ hasValidKey: isValid,
+ keyLastTested: isValid ? new Date().toISOString() : state.settings.api.keyLastTested,
+ },
+ },
+ hasUnsavedChanges: true,
+ }));
+ },
+
+ removeApiKey: () => {
+ set((state) => ({
+ settings: {
+ ...state.settings,
+ api: {
+ hasValidKey: false,
+ },
+ },
+ hasUnsavedChanges: true,
+ }));
+ },
+
+ saveSettings: async () => {
+ const { settings } = get();
+ set({ isSaving: true });
+
+ try {
+ // Simulate API call to save settings
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // In a real app, make API call here:
+ // await settingsApi.update(settings);
+
+ set({
+ hasUnsavedChanges: false,
+ lastSaved: new Date().toISOString()
+ });
+ } catch (error) {
+ console.error('Failed to save settings:', error);
+ throw error;
+ } finally {
+ set({ isSaving: false });
+ }
+ },
+
+ loadSettings: async () => {
+ set({ isLoading: true });
+
+ try {
+ // Simulate API call to load settings
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ // In a real app, make API call here:
+ // const userSettings = await settingsApi.get();
+ // set({ settings: userSettings });
+
+ // For now, use default settings
+ set({
+ settings: defaultSettings,
+ hasUnsavedChanges: false,
+ });
+ } catch (error) {
+ console.error('Failed to load settings:', error);
+ throw error;
+ } finally {
+ set({ isLoading: false });
+ }
+ },
+
+ resetToDefaults: () => {
+ set({
+ settings: defaultSettings,
+ hasUnsavedChanges: true,
+ });
+ },
+
+ // Utilities
+ markAsUnsaved: () => set({ hasUnsavedChanges: true }),
+ markAsSaved: () => set({
+ hasUnsavedChanges: false,
+ lastSaved: new Date().toISOString()
+ }),
+ }))
+);
\ No newline at end of file