diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx
index 14e3ca9a..deb8f6a5 100644
--- a/src/AppRouter.tsx
+++ b/src/AppRouter.tsx
@@ -11,6 +11,11 @@ import { NIP19Page } from "./pages/NIP19Page";
import { NotificationsPage } from "./pages/NotificationsPage";
import { SearchPage } from "./pages/SearchPage";
import { SettingsPage } from "./pages/SettingsPage";
+import { ProfileSettings } from "./pages/ProfileSettings";
+import { AppearanceSettings } from "./pages/AppearanceSettings";
+import { WalletSettingsPage } from "./pages/WalletSettingsPage";
+import { NotificationSettings } from "./pages/NotificationSettings";
+import { AdvancedSettingsPage } from "./pages/AdvancedSettingsPage";
import { HashtagPage } from "./pages/HashtagPage";
import { DomainFeedPage } from "./pages/DomainFeedPage";
import { BookmarksPage } from "./pages/BookmarksPage";
@@ -40,7 +45,12 @@ export function AppRouter() {
} />
} />
} />
- } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
} />} />
} />} />
} />
diff --git a/src/components/AdvancedSettings.tsx b/src/components/AdvancedSettings.tsx
index 39806538..c9720633 100644
--- a/src/components/AdvancedSettings.tsx
+++ b/src/components/AdvancedSettings.tsx
@@ -4,7 +4,6 @@ import { Button } from '@/components/ui/button';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
-import { WalletSettings } from '@/components/WalletSettings';
import { RelayListManager } from '@/components/RelayListManager';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAppContext } from '@/hooks/useAppContext';
@@ -14,7 +13,6 @@ export function AdvancedSettings() {
const { user } = useCurrentUser();
const { config, updateConfig } = useAppContext();
const { toast } = useToast();
- const [walletOpen, setWalletOpen] = useState(false);
const [relaysOpen, setRelaysOpen] = useState(false);
const [statsOpen, setStatsOpen] = useState(false);
const [servicesOpen, setServicesOpen] = useState(false);
@@ -74,35 +72,6 @@ export function AdvancedSettings() {
- {/* Wallet Section — only when logged in */}
- {user && (
-
diff --git a/src/components/ThemeSelector.tsx b/src/components/ThemeSelector.tsx
new file mode 100644
index 00000000..c7cee8bd
--- /dev/null
+++ b/src/components/ThemeSelector.tsx
@@ -0,0 +1,128 @@
+import { Check, Plus } from 'lucide-react';
+import { type Theme } from '@/contexts/AppContext';
+import { useTheme } from '@/hooks/useTheme';
+import { themes, type ThemeTokens } from '@/themes';
+import { cn } from '@/lib/utils';
+
+interface ThemeOption {
+ id: Theme | 'custom';
+ label: string;
+ tokens?: ThemeTokens;
+}
+
+const themeOptions: ThemeOption[] = [
+ { id: 'light', label: 'Light', tokens: themes.light },
+ { id: 'dark', label: 'Dark', tokens: themes.dark },
+ { id: 'black', label: 'Black', tokens: themes.black },
+ { id: 'pink', label: 'Pink', tokens: themes.pink },
+ { id: 'custom', label: 'Custom' },
+];
+
+/** Extracts HSL color string from a theme token value like "258 70% 55%" */
+function hsl(value: string): string {
+ return `hsl(${value})`;
+}
+
+export function ThemeSelector() {
+ const { theme, setTheme } = useTheme();
+
+ return (
+
+
+ {themeOptions.map((option) => {
+ if (option.id === 'custom') {
+ return (
+
+ );
+ }
+
+ const tokens = option.tokens!;
+ const isActive = theme === option.id;
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/pages/AdvancedSettingsPage.tsx b/src/pages/AdvancedSettingsPage.tsx
new file mode 100644
index 00000000..41d4705f
--- /dev/null
+++ b/src/pages/AdvancedSettingsPage.tsx
@@ -0,0 +1,34 @@
+import { useSeoMeta } from '@unhead/react';
+import { ArrowLeft } from 'lucide-react';
+import { Link } from 'react-router-dom';
+import { AdvancedSettings } from '@/components/AdvancedSettings';
+
+export function AdvancedSettingsPage() {
+ useSeoMeta({
+ title: 'Advanced | Settings | Ditto',
+ description: 'Advanced settings for relays, upload servers, and system configuration',
+ });
+
+ return (
+
+ {/* Header with back link */}
+
+
+
+
+
+
+
Advanced
+
+ Relays, upload servers, and system settings
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/AppearanceSettings.tsx b/src/pages/AppearanceSettings.tsx
new file mode 100644
index 00000000..12edc1fa
--- /dev/null
+++ b/src/pages/AppearanceSettings.tsx
@@ -0,0 +1,83 @@
+import { useSeoMeta } from '@unhead/react';
+import { ArrowLeft } from 'lucide-react';
+import { Link, useParams } from 'react-router-dom';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { ThemeSelector } from '@/components/ThemeSelector';
+import { ContentSettings } from '@/components/ContentSettings';
+import { cn, STICKY_HEADER_CLASS } from '@/lib/utils';
+
+type AppearanceTab = 'theme' | 'content';
+
+export function AppearanceSettings() {
+ const { tab } = useParams<{ tab?: string }>();
+ const activeTab: AppearanceTab = tab === 'content' ? 'content' : 'theme';
+
+ useSeoMeta({
+ title: 'Appearance | Settings | Ditto',
+ description: 'Customize your display preferences and content settings',
+ });
+
+ return (
+
+ {/* Header with back link */}
+
+
+
+
+
+
+
Appearance
+
+ Theme, display preferences, and content settings
+
+
+
+
+
+ {/* Sub-section tabs */}
+
+
+ {/* Content */}
+
+ {activeTab === 'theme' ? (
+
+ {/* Theme selector */}
+
+
+ Theme
+
+ Choose a theme for the interface
+
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+function AppearanceTabLink({ to, label, active }: { to: string; label: string; active: boolean }) {
+ return (
+
+ {label}
+ {active && (
+
+ )}
+
+ );
+}
diff --git a/src/pages/NotificationSettings.tsx b/src/pages/NotificationSettings.tsx
new file mode 100644
index 00000000..64daa934
--- /dev/null
+++ b/src/pages/NotificationSettings.tsx
@@ -0,0 +1,124 @@
+import { useState, useEffect } from 'react';
+import { useSeoMeta } from '@unhead/react';
+import { ArrowLeft, Bell, BellOff } from 'lucide-react';
+import { Link, Navigate } from 'react-router-dom';
+import { Card, CardContent } from '@/components/ui/card';
+import { Switch } from '@/components/ui/switch';
+import { Label } from '@/components/ui/label';
+import { Badge } from '@/components/ui/badge';
+import { useCurrentUser } from '@/hooks/useCurrentUser';
+
+export function NotificationSettings() {
+ const { user } = useCurrentUser();
+ const [pushEnabled, setPushEnabled] = useState(false);
+ const [permission, setPermission] = useState
('default');
+
+ useSeoMeta({
+ title: 'Notifications | Settings | Ditto',
+ description: 'Configure your notification preferences',
+ });
+
+ // Check current permission state on mount
+ useEffect(() => {
+ if ('Notification' in window) {
+ setPermission(Notification.permission);
+ setPushEnabled(Notification.permission === 'granted');
+ }
+ }, []);
+
+ const handleTogglePush = async (enabled: boolean) => {
+ if (!('Notification' in window)) return;
+
+ if (enabled) {
+ const result = await Notification.requestPermission();
+ setPermission(result);
+ setPushEnabled(result === 'granted');
+ } else {
+ // Browser doesn't allow revoking permissions programmatically,
+ // but we can track the user's preference
+ setPushEnabled(false);
+ }
+ };
+
+ if (!user) {
+ return ;
+ }
+
+ const isSupported = 'Notification' in window;
+ const isDenied = permission === 'denied';
+
+ return (
+
+ {/* Header with back link */}
+
+
+
+
+
+
+
Notifications
+
+ Configure push notification preferences
+
+
+
+
+
+
+ {/* Push notifications toggle */}
+
+
+
+
+
+ {pushEnabled ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ Receive notifications for mentions, replies, and zaps
+
+
+
+
+
+
+ {/* Status info */}
+ {!isSupported && (
+
+
+ Not Supported
+
+
+ Your browser does not support push notifications.
+
+
+ )}
+
+ {isDenied && (
+
+
+ Blocked
+
+
+ Notifications are blocked. Update your browser settings to allow notifications from this site.
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/pages/ProfileSettings.tsx b/src/pages/ProfileSettings.tsx
new file mode 100644
index 00000000..618d2cc4
--- /dev/null
+++ b/src/pages/ProfileSettings.tsx
@@ -0,0 +1,41 @@
+import { useSeoMeta } from '@unhead/react';
+import { ArrowLeft } from 'lucide-react';
+import { Link, Navigate } from 'react-router-dom';
+import { EditProfileForm } from '@/components/EditProfileForm';
+import { useCurrentUser } from '@/hooks/useCurrentUser';
+
+export function ProfileSettings() {
+ const { user } = useCurrentUser();
+
+ useSeoMeta({
+ title: 'Profile | Settings | Ditto',
+ description: 'Edit your Ditto profile',
+ });
+
+ if (!user) {
+ return ;
+ }
+
+ return (
+
+ {/* Header with back link */}
+
+
+
+
+
+
+
Profile
+
+ Edit your display name, bio, and avatar
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx
index c1106a6a..fe9bd01c 100644
--- a/src/pages/SettingsPage.tsx
+++ b/src/pages/SettingsPage.tsx
@@ -1,73 +1,118 @@
import { useSeoMeta } from '@unhead/react';
-import { ArrowLeft, Settings as SettingsIcon } from 'lucide-react';
-import { Link, useParams } from 'react-router-dom';
-import { EditProfileForm } from '@/components/EditProfileForm';
-import { ContentSettings } from '@/components/ContentSettings';
-import { AdvancedSettings } from '@/components/AdvancedSettings';
+import { ArrowLeft, Bell, ChevronRight, Palette, Server, Settings as SettingsIcon, User, Wallet } from 'lucide-react';
+import { Link, useNavigate } from 'react-router-dom';
+import { Card, CardContent } from '@/components/ui/card';
import { useCurrentUser } from '@/hooks/useCurrentUser';
-import { cn, STICKY_HEADER_CLASS } from '@/lib/utils';
+
+interface SettingsSection {
+ id: string;
+ label: string;
+ description: string;
+ icon: React.ComponentType<{ className?: string }>;
+ path: string;
+ requiresAuth?: boolean;
+}
+
+const sections: SettingsSection[] = [
+ {
+ id: 'profile',
+ label: 'Profile',
+ description: 'Edit your display name, bio, and avatar',
+ icon: User,
+ path: '/settings/profile',
+ requiresAuth: true,
+ },
+ {
+ id: 'appearance',
+ label: 'Appearance',
+ description: 'Theme, display preferences, and content settings',
+ icon: Palette,
+ path: '/settings/appearance',
+ },
+ {
+ id: 'wallet',
+ label: 'Wallet',
+ description: 'Manage wallet connections and payments',
+ icon: Wallet,
+ path: '/settings/wallet',
+ requiresAuth: true,
+ },
+ {
+ id: 'notifications',
+ label: 'Notifications',
+ description: 'Configure push notification preferences',
+ icon: Bell,
+ path: '/settings/notifications',
+ requiresAuth: true,
+ },
+ {
+ id: 'advanced',
+ label: 'Advanced',
+ description: 'Relays, upload servers, and system settings',
+ icon: Server,
+ path: '/settings/advanced',
+ },
+];
export function SettingsPage() {
- const { section } = useParams<{ section?: string }>();
const { user } = useCurrentUser();
+ const navigate = useNavigate();
useSeoMeta({
title: 'Settings | Ditto',
description: 'Manage your Ditto settings',
});
- const activeSection = section || (user ? 'profile' : 'content');
+ const visibleSections = sections.filter(
+ (section) => !section.requiresAuth || user,
+ );
return (
-
-
-
-
-
-
-
-
-
-
Settings
-
-
Customize your profile, content, and advanced features
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+
Settings
+
+ Customize your experience
+
+
- {/* Tab navigation */}
-
- {user && }
-
-
-
-
-
- {activeSection === 'content' ? (
-
- ) : activeSection === 'advanced' ? (
-
- ) : user && activeSection === 'profile' ? (
-
- ) : null}
-
-
- );
-}
-
-function SettingsTab({ to, label, active }: { to: string; label: string; active: boolean }) {
- return (
-
- {label}
- {active && (
-
- )}
-
+ {/* Settings menu */}
+
+ {visibleSections.map((section) => {
+ const Icon = section.icon;
+ return (
+
navigate(section.path)}
+ >
+
+
+
+
+
+
{section.label}
+
+ {section.description}
+
+
+
+
+
+ );
+ })}
+
+
);
}
diff --git a/src/pages/WalletSettingsPage.tsx b/src/pages/WalletSettingsPage.tsx
new file mode 100644
index 00000000..b0d746b1
--- /dev/null
+++ b/src/pages/WalletSettingsPage.tsx
@@ -0,0 +1,41 @@
+import { useSeoMeta } from '@unhead/react';
+import { ArrowLeft } from 'lucide-react';
+import { Link, Navigate } from 'react-router-dom';
+import { WalletSettings } from '@/components/WalletSettings';
+import { useCurrentUser } from '@/hooks/useCurrentUser';
+
+export function WalletSettingsPage() {
+ const { user } = useCurrentUser();
+
+ useSeoMeta({
+ title: 'Wallet | Settings | Ditto',
+ description: 'Manage your wallet connections',
+ });
+
+ if (!user) {
+ return
;
+ }
+
+ return (
+
+ {/* Header with back link */}
+
+
+
+
+
+
+
Wallet
+
+ Manage wallet connections and payments
+
+
+
+
+
+
+
+
+
+ );
+}