Reorganize Settings into menu-based navigation with sub-pages
Replace the single tabbed Settings page with a landing menu that navigates to dedicated sub-pages, following the Agora pattern: - Settings landing: card-based menu with icons and descriptions - Profile: wraps EditProfileForm (auth-gated) - Appearance: Theme sub-tab with visual theme selector (light/dark/black/pink + custom placeholder) and Content sub-tab with existing content settings - Wallet: dedicated page for NWC wallet management (auth-gated) - Notifications: push notification enable/disable toggle (auth-gated) - Advanced: network relays, stats source, and system settings (wallet section removed since it has its own page now)
This commit is contained in:
+11
-1
@@ -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() {
|
||||
<Route path="/t/:tag" element={<HashtagPage />} />
|
||||
<Route path="/timeline/:domain" element={<DomainFeedPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/settings/:section" element={<SettingsPage />} />
|
||||
<Route path="/settings/profile" element={<ProfileSettings />} />
|
||||
<Route path="/settings/appearance" element={<AppearanceSettings />} />
|
||||
<Route path="/settings/appearance/:tab" element={<AppearanceSettings />} />
|
||||
<Route path="/settings/wallet" element={<WalletSettingsPage />} />
|
||||
<Route path="/settings/notifications" element={<NotificationSettings />} />
|
||||
<Route path="/settings/advanced" element={<AdvancedSettingsPage />} />
|
||||
<Route path="/vines" element={<KindFeedPage kind={34236} title="Vines" icon={<Clapperboard className="size-5" />} />} />
|
||||
<Route path="/polls" element={<KindFeedPage kind={1068} title="Polls" icon={<BarChart3 className="size-5" />} />} />
|
||||
<Route path="/treasures" element={<TreasuresPage />} />
|
||||
|
||||
@@ -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() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Wallet Section — only when logged in */}
|
||||
{user && (
|
||||
<div>
|
||||
<Collapsible open={walletOpen} onOpenChange={setWalletOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="relative w-full justify-between px-3 py-3.5 h-auto hover:bg-muted/20 hover:text-foreground rounded-none"
|
||||
>
|
||||
<span className="text-base font-semibold">Wallet</span>
|
||||
{walletOpen ? (
|
||||
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-1 bg-primary rounded-full" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="pb-4 pt-4">
|
||||
<div className="px-3">
|
||||
<WalletSettings />
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Network (Relays) Section — only when logged in */}
|
||||
{user && (
|
||||
<div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
{themeOptions.map((option) => {
|
||||
if (option.id === 'custom') {
|
||||
return (
|
||||
<button
|
||||
key="custom"
|
||||
className={cn(
|
||||
'relative group rounded-xl border-2 border-dashed border-muted-foreground/25 p-1 transition-all hover:border-primary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||
)}
|
||||
onClick={() => {
|
||||
// Custom theme builder placeholder -- navigates or opens modal in future
|
||||
}}
|
||||
title="Custom theme builder (coming soon)"
|
||||
>
|
||||
<div className="aspect-[4/3] rounded-lg bg-muted/30 flex flex-col items-center justify-center gap-1.5">
|
||||
<Plus className="size-5 text-muted-foreground/50" />
|
||||
<span className="text-[10px] font-medium text-muted-foreground/60">Coming soon</span>
|
||||
</div>
|
||||
<p className="mt-1.5 text-xs font-medium text-center text-muted-foreground">
|
||||
{option.label}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const tokens = option.tokens!;
|
||||
const isActive = theme === option.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
className={cn(
|
||||
'relative group rounded-xl border-2 p-1 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||
isActive
|
||||
? 'border-primary shadow-sm'
|
||||
: 'border-border hover:border-primary/40',
|
||||
)}
|
||||
onClick={() => setTheme(option.id as Theme)}
|
||||
>
|
||||
{/* Mini preview */}
|
||||
<div
|
||||
className="aspect-[4/3] rounded-lg overflow-hidden relative"
|
||||
style={{ backgroundColor: hsl(tokens.background) }}
|
||||
>
|
||||
{/* Simulated header bar */}
|
||||
<div
|
||||
className="h-2.5 w-full"
|
||||
style={{ backgroundColor: hsl(tokens.card) }}
|
||||
/>
|
||||
{/* Content preview area */}
|
||||
<div className="p-1.5 space-y-1">
|
||||
{/* Simulated text lines */}
|
||||
<div
|
||||
className="h-1 w-3/4 rounded-full"
|
||||
style={{ backgroundColor: hsl(tokens.foreground), opacity: 0.6 }}
|
||||
/>
|
||||
<div
|
||||
className="h-1 w-1/2 rounded-full"
|
||||
style={{ backgroundColor: hsl(tokens.mutedForeground), opacity: 0.4 }}
|
||||
/>
|
||||
{/* Simulated button */}
|
||||
<div className="pt-0.5">
|
||||
<div
|
||||
className="h-2 w-8 rounded-sm"
|
||||
style={{ backgroundColor: hsl(tokens.primary) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Simulated sidebar strip */}
|
||||
<div
|
||||
className="absolute right-0 top-0 bottom-0 w-4"
|
||||
style={{ backgroundColor: hsl(tokens.sidebarBackground) }}
|
||||
/>
|
||||
|
||||
{/* Active check mark */}
|
||||
{isActive && (
|
||||
<div className="absolute top-1 left-1 size-4 rounded-full flex items-center justify-center"
|
||||
style={{ backgroundColor: hsl(tokens.primary) }}
|
||||
>
|
||||
<Check className="size-2.5" style={{ color: hsl(tokens.primaryForeground) }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<p className={cn(
|
||||
'mt-1.5 text-xs font-medium text-center transition-colors',
|
||||
isActive ? 'text-foreground' : 'text-muted-foreground',
|
||||
)}>
|
||||
{option.label}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<main className="flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l xl:border-r border-border min-h-screen">
|
||||
{/* Header with back link */}
|
||||
<div className="px-4 pt-4 pb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/settings" className="p-2 -ml-2 rounded-full hover:bg-secondary transition-colors">
|
||||
<ArrowLeft className="size-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Advanced</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Relays, upload servers, and system settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<AdvancedSettings />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<main className="flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l xl:border-r border-border min-h-screen">
|
||||
{/* Header with back link */}
|
||||
<div className="px-4 pt-4 pb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/settings" className="p-2 -ml-2 rounded-full hover:bg-secondary transition-colors">
|
||||
<ArrowLeft className="size-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Appearance</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Theme, display preferences, and content settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sub-section tabs */}
|
||||
<div className={cn(STICKY_HEADER_CLASS, 'flex border-b border-border bg-background/80 backdrop-blur-md z-10')}>
|
||||
<AppearanceTabLink to="/settings/appearance" label="Theme" active={activeTab === 'theme'} />
|
||||
<AppearanceTabLink to="/settings/appearance/content" label="Content" active={activeTab === 'content'} />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
{activeTab === 'theme' ? (
|
||||
<div className="space-y-6">
|
||||
{/* Theme selector */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">Theme</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose a theme for the interface
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ThemeSelector />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<ContentSettings />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function AppearanceTabLink({ to, label, active }: { to: string; label: string; active: boolean }) {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className={cn(
|
||||
'flex-1 py-3.5 text-center text-sm font-medium transition-colors relative hover:bg-secondary/40',
|
||||
active ? 'text-foreground' : 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{active && (
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-16 h-1 bg-primary rounded-full" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -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<NotificationPermission>('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 <Navigate to="/settings" replace />;
|
||||
}
|
||||
|
||||
const isSupported = 'Notification' in window;
|
||||
const isDenied = permission === 'denied';
|
||||
|
||||
return (
|
||||
<main className="flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l xl:border-r border-border min-h-screen">
|
||||
{/* Header with back link */}
|
||||
<div className="px-4 pt-4 pb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/settings" className="p-2 -ml-2 rounded-full hover:bg-secondary transition-colors">
|
||||
<ArrowLeft className="size-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Notifications</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Configure push notification preferences
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Push notifications toggle */}
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center size-10 rounded-full bg-secondary shrink-0">
|
||||
{pushEnabled ? (
|
||||
<Bell className="size-5 text-muted-foreground" />
|
||||
) : (
|
||||
<BellOff className="size-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="push-notifications" className="text-sm font-semibold cursor-pointer">
|
||||
Push Notifications
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Receive notifications for mentions, replies, and zaps
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
id="push-notifications"
|
||||
checked={pushEnabled}
|
||||
onCheckedChange={handleTogglePush}
|
||||
disabled={!isSupported || isDenied}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status info */}
|
||||
{!isSupported && (
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Not Supported
|
||||
</Badge>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Your browser does not support push notifications.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDenied && (
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
Blocked
|
||||
</Badge>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Notifications are blocked. Update your browser settings to allow notifications from this site.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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 <Navigate to="/settings" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l xl:border-r border-border min-h-screen">
|
||||
{/* Header with back link */}
|
||||
<div className="px-4 pt-4 pb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/settings" className="p-2 -ml-2 rounded-full hover:bg-secondary transition-colors">
|
||||
<ArrowLeft className="size-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Profile</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Edit your display name, bio, and avatar
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<EditProfileForm />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+99
-54
@@ -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 (
|
||||
<main className="flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l xl:border-r border-border min-h-screen">
|
||||
<div className="px-4 pt-4 pb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/" className="p-2 rounded-full hover:bg-secondary transition-colors sidebar:hidden">
|
||||
<ArrowLeft className="size-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsIcon className="size-5" />
|
||||
<h1 className="text-xl font-bold">Settings</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Customize your profile, content, and advanced features</p>
|
||||
<main className="flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l xl:border-r border-border min-h-screen">
|
||||
{/* Header */}
|
||||
<div className="px-4 pt-4 pb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/" className="p-2 rounded-full hover:bg-secondary transition-colors sidebar:hidden">
|
||||
<ArrowLeft className="size-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsIcon className="size-5" />
|
||||
<h1 className="text-xl font-bold">Settings</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Customize your experience
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab navigation */}
|
||||
<div className={cn(STICKY_HEADER_CLASS, 'flex border-b border-border bg-background/80 backdrop-blur-md z-10')}>
|
||||
{user && <SettingsTab to="/settings/profile" label="Profile" active={activeSection === 'profile'} />}
|
||||
<SettingsTab to="/settings/content" label="Content" active={activeSection === 'content'} />
|
||||
<SettingsTab to="/settings/advanced" label="Advanced" active={activeSection === 'advanced'} />
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
{activeSection === 'content' ? (
|
||||
<ContentSettings />
|
||||
) : activeSection === 'advanced' ? (
|
||||
<AdvancedSettings />
|
||||
) : user && activeSection === 'profile' ? (
|
||||
<EditProfileForm />
|
||||
) : null}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsTab({ to, label, active }: { to: string; label: string; active: boolean }) {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className={cn(
|
||||
'flex-1 py-3.5 text-center text-sm font-medium transition-colors relative hover:bg-secondary/40',
|
||||
active ? 'text-foreground' : 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{active && (
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-16 h-1 bg-primary rounded-full" />
|
||||
)}
|
||||
</Link>
|
||||
{/* Settings menu */}
|
||||
<div className="p-4 space-y-2">
|
||||
{visibleSections.map((section) => {
|
||||
const Icon = section.icon;
|
||||
return (
|
||||
<Card
|
||||
key={section.id}
|
||||
className="cursor-pointer transition-colors hover:bg-muted/40 active:bg-muted/60"
|
||||
onClick={() => navigate(section.path)}
|
||||
>
|
||||
<CardContent className="flex items-center gap-4 p-4">
|
||||
<div className="flex items-center justify-center size-10 rounded-full bg-secondary shrink-0">
|
||||
<Icon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold">{section.label}</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{section.description}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="size-5 text-muted-foreground shrink-0" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 <Navigate to="/settings" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l xl:border-r border-border min-h-screen">
|
||||
{/* Header with back link */}
|
||||
<div className="px-4 pt-4 pb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/settings" className="p-2 -ml-2 rounded-full hover:bg-secondary transition-colors">
|
||||
<ArrowLeft className="size-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Wallet</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Manage wallet connections and payments
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<WalletSettings />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user