Compare commits

...

2 Commits

Author SHA1 Message Date
sam d235a470c0 remove double settings link 2026-05-21 23:23:43 +07:00
sam 8b7a7af323 replace settings icons, ensure settings are accesssable 2026-05-21 23:09:05 +07:00
15 changed files with 145 additions and 157 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+1
View File
@@ -155,6 +155,7 @@ function NavLinkButton({ item }: { item: NavItem }) {
function MobileFooterLinks({ onClose }: { onClose: () => void }) {
const items = [
{ label: 'Settings', to: '/settings' },
{ label: 'Privacy', to: '/privacy' },
{ label: 'Safety', to: '/safety' },
{ label: 'Changelog', to: '/changelog' },
+12 -19
View File
@@ -1,14 +1,13 @@
import { useSeoMeta } from '@unhead/react';
import { ChevronDown, ChevronUp, Settings2 } from 'lucide-react';
import { useState } from 'react';
import { PageHeader } from '@/components/PageHeader';
import { IntroImage } from '@/components/IntroImage';
import { AdvancedSettings } from '@/components/AdvancedSettings';
import { WalletSettings } from '@/components/WalletSettings';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAppContext } from '@/hooks/useAppContext';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Button } from '@/components/ui/button';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useState } from 'react';
export function AdvancedSettingsPage() {
const { user } = useCurrentUser();
@@ -27,27 +26,21 @@ export function AdvancedSettingsPage() {
backTo="/settings"
alwaysShowBack
titleContent={
<div className="flex-1 min-w-0">
<h1 className="text-xl font-bold">Advanced</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Wallet connections, system configuration, and other advanced options for power users.
</p>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-center size-11 rounded-xl shrink-0 bg-muted">
<Settings2 className="size-5 text-muted-foreground" />
</div>
<div className="min-w-0">
<h1 className="text-xl font-bold">Advanced</h1>
<p className="text-sm text-muted-foreground mt-0.5 leading-snug">
Wallet, system, and power user settings.
</p>
</div>
</div>
}
/>
<div className="p-4">
{/* Intro */}
<div className="flex items-center gap-4 px-3 pt-2 pb-4">
<IntroImage src="/advanced-intro.png" />
<div className="min-w-0">
<h2 className="text-sm font-semibold">Power User Settings</h2>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
Wallet connections, system configuration, and other advanced options.
</p>
</div>
</div>
{/* Wallet collapsible — only when logged in */}
{user && (
<Collapsible open={walletOpen} onOpenChange={setWalletOpen}>
+11 -18
View File
@@ -1,7 +1,6 @@
import { useSeoMeta } from '@unhead/react';
import { Monitor, Moon, Sun } from 'lucide-react';
import { Monitor, Moon, Palette, Sun } from 'lucide-react';
import { PageHeader } from '@/components/PageHeader';
import { IntroImage } from '@/components/IntroImage';
import { useAppContext } from '@/hooks/useAppContext';
import { useTheme } from '@/hooks/useTheme';
import { cn } from '@/lib/utils';
@@ -53,27 +52,21 @@ export function AppearanceSettingsPage() {
backTo="/settings"
alwaysShowBack
titleContent={
<div className="flex-1 min-w-0">
<h1 className="text-xl font-bold">Appearance</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Choose how the app looks.
</p>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-center size-11 rounded-xl shrink-0 bg-violet-500/10">
<Palette className="size-5 text-violet-500" />
</div>
<div className="min-w-0">
<h1 className="text-xl font-bold">Appearance</h1>
<p className="text-sm text-muted-foreground mt-0.5 leading-snug">
Light, dark, or system theme.
</p>
</div>
</div>
}
/>
<div className="p-4">
{/* Intro */}
<div className="flex items-center gap-4 px-3 pt-2 pb-6">
<IntroImage src="/theme-intro.png" />
<div className="min-w-0">
<h2 className="text-sm font-semibold">Color Mode</h2>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
Pick your preferred color mode. System will automatically match your device's light or dark setting.
</p>
</div>
</div>
{/* Theme options */}
<div className="space-y-2">
{themeOptions.map((option) => (
+11 -18
View File
@@ -1,10 +1,9 @@
import { useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { RotateCcw } from 'lucide-react';
import { Filter, RotateCcw } from 'lucide-react';
import { MuteSettingsInternals, SensitiveContentSection } from '@/components/ContentSettings';
import { MuteListRecoveryDialog } from '@/components/MuteListRecoveryDialog';
import { PageHeader } from '@/components/PageHeader';
import { IntroImage } from '@/components/IntroImage';
import { HelpTip } from '@/components/HelpTip';
import { Button } from '@/components/ui/button';
import { useAppContext } from '@/hooks/useAppContext';
@@ -27,26 +26,20 @@ export function ContentPage() {
backTo="/settings"
alwaysShowBack
titleContent={
<div className="flex-1 min-w-0">
<h1 className="text-xl font-bold">Content</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Control what you see. Mute users, hashtags, or words, and choose how content warnings are handled. Mutes are encrypted and private.
</p>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-center size-11 rounded-xl shrink-0 bg-orange-500/10">
<Filter className="size-5 text-orange-500" />
</div>
<div className="min-w-0">
<h1 className="text-xl font-bold">Content</h1>
<p className="text-sm text-muted-foreground mt-0.5 leading-snug">
Muted users, hashtags, and content warnings.
</p>
</div>
</div>
}
/>
{/* Lead image — Muted Content */}
<div className="flex items-center gap-4 px-7 py-5">
<IntroImage src="/mute-intro.png" size="w-28" />
<div className="min-w-0">
<h2 className="text-base font-semibold">Content Control</h2>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
Hide posts from specific users, hashtags, words, or entire threads. All mutes are encrypted and private.
</p>
</div>
</div>
<div className="p-4 space-y-0">
{/* Muted Content Section */}
+11 -5
View File
@@ -1,4 +1,5 @@
import { useSeoMeta } from '@unhead/react';
import { LayoutList } from 'lucide-react';
import { ContentSettings } from '@/components/ContentSettings';
import { PageHeader } from '@/components/PageHeader';
import { HelpTip } from '@/components/HelpTip';
@@ -19,11 +20,16 @@ export function ContentSettingsPage() {
backTo="/settings"
alwaysShowBack
titleContent={
<div className="flex-1 min-w-0">
<h1 className="text-xl font-bold flex items-center gap-1.5">Home Feed <HelpTip faqId="fyp" /></h1>
<p className="text-sm text-muted-foreground mt-0.5">
Nostr supports many content types beyond text posts. Customize which appear in your home feed.
</p>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-center size-11 rounded-xl shrink-0 bg-sky-500/10">
<LayoutList className="size-5 text-sky-500" />
</div>
<div className="min-w-0">
<h1 className="text-xl font-bold flex items-center gap-1.5">Home Feed <HelpTip faqId="fyp" /></h1>
<p className="text-sm text-muted-foreground mt-0.5 leading-snug">
Choose which content types appear in your feed.
</p>
</div>
</div>
}
/>
+11 -30
View File
@@ -1,6 +1,6 @@
import { useSeoMeta } from '@unhead/react';
import { Sparkles } from 'lucide-react';
import { PageHeader } from '@/components/PageHeader';
import { IntroImage } from '@/components/IntroImage';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { useAppContext } from '@/hooks/useAppContext';
@@ -24,34 +24,21 @@ export function MagicSettingsPage() {
backTo="/settings"
alwaysShowBack
titleContent={
<div className="flex-1 min-w-0">
<h1 className="text-xl font-bold">Magic</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Harness the mystical energies of your device. Imbue your cursor with elemental fire.
</p>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-center size-11 rounded-xl shrink-0 bg-primary/10">
<Sparkles className="size-5 text-primary" />
</div>
<div className="min-w-0">
<h1 className="text-xl font-bold">Magic</h1>
<p className="text-sm text-muted-foreground mt-0.5 leading-snug">
Enchanted cursor effects.
</p>
</div>
</div>
}
/>
<div className="p-4">
{/* Intro */}
<div className="flex items-center gap-4 px-3 pt-2 pb-6">
<IntroImage src="/magic-intro.png" />
<div className="min-w-0">
<h2 className="text-sm font-semibold">Arcane Configuration</h2>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
Harness the mystical energies of your device. Imbue your cursor with elemental fire and make every interaction feel enchanted.
</p>
</div>
</div>
{/* Ornament */}
<div className="flex items-center gap-3 px-2 pb-5">
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-primary/40 to-primary/60" />
<span className="text-primary/50 text-xs tracking-[0.3em] select-none"></span>
<div className="h-px flex-1 bg-gradient-to-l from-transparent via-primary/40 to-primary/60" />
</div>
{/* Magic Mouse toggle */}
<div
className="flex items-start gap-4 rounded-xl px-4 py-4 transition-colors hover:bg-muted/40"
@@ -73,12 +60,6 @@ export function MagicSettingsPage() {
/>
</div>
{/* Bottom ornament */}
<div className="flex items-center gap-3 px-2 pt-6 pb-4">
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-primary/20 to-primary/30" />
<span className="text-primary/30 text-[10px] tracking-[0.4em] select-none"></span>
<div className="h-px flex-1 bg-gradient-to-l from-transparent via-primary/20 to-primary/30" />
</div>
</div>
</main>
);
+11 -16
View File
@@ -1,9 +1,9 @@
import { useSeoMeta } from '@unhead/react';
import { Radio } from 'lucide-react';
import { Navigate } from 'react-router-dom';
import { PageHeader } from '@/components/PageHeader';
import { RelayListManager } from '@/components/RelayListManager';
import { BlossomSettings } from '@/components/BlossomSettings';
import { IntroImage } from '@/components/IntroImage';
import { HelpTip } from '@/components/HelpTip';
import { Label } from '@/components/ui/label';
import { useCurrentUser } from '@/hooks/useCurrentUser';
@@ -30,26 +30,21 @@ export function NetworkSettingsPage() {
backTo="/settings"
alwaysShowBack
titleContent={
<div className="flex-1 min-w-0">
<h1 className="text-xl font-bold flex items-center gap-1.5">Network <HelpTip faqId="what-is-nostr" /></h1>
<p className="text-sm text-muted-foreground mt-0.5">
Relays are servers that store and distribute content across the Nostr network. Blossom servers handle file uploads.
</p>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-center size-11 rounded-xl shrink-0 bg-emerald-500/10">
<Radio className="size-5 text-emerald-500" />
</div>
<div className="min-w-0">
<h1 className="text-xl font-bold flex items-center gap-1.5">Network <HelpTip faqId="what-is-nostr" /></h1>
<p className="text-sm text-muted-foreground mt-0.5 leading-snug">
Relays and file upload servers.
</p>
</div>
</div>
}
/>
<div className="p-4">
{/* Intro */}
<div className="flex items-center gap-4 px-3 pt-2 pb-4">
<IntroImage src="/relay-intro.png" />
<div className="min-w-0">
<h2 className="text-sm font-semibold">Network Connections</h2>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
Manage your relay connections. Relays are servers that store and distribute Nostr events across the network.
</p>
</div>
</div>
{/* Relays */}
<div>
+10 -5
View File
@@ -272,11 +272,16 @@ export function NotificationSettings() {
backTo="/settings"
alwaysShowBack
titleContent={
<div className="flex-1 min-w-0">
<h1 className="text-xl font-bold">Notifications</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Customize which notifications you receive.
</p>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-center size-11 rounded-xl shrink-0 bg-yellow-500/10">
<Bell className="size-5 text-yellow-500" />
</div>
<div className="min-w-0">
<h1 className="text-xl font-bold">Notifications</h1>
<p className="text-sm text-muted-foreground mt-0.5 leading-snug">
Choose when and how Agora alerts you.
</p>
</div>
</div>
}
/>
+9 -16
View File
@@ -2,7 +2,7 @@ import { useSeoMeta } from '@unhead/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Loader2, Plus, Trash2, ChevronDown,
Wallet, Upload, Music, ImageIcon, Film, Mail, Link2, Pencil, Eye, EyeOff, Copy, Check, Download, KeyRound, AlertTriangle, CloudSun,
Wallet, Upload, Music, ImageIcon, Film, Mail, Link2, Pencil, Eye, EyeOff, Copy, Check, Download, KeyRound, AlertTriangle, CloudSun, User,
} from 'lucide-react';
import { nip19 } from 'nostr-tools';
import { useNostrLogin } from '@nostrify/react/login';
@@ -20,7 +20,6 @@ import { useQueryClient } from '@tanstack/react-query';
import { ProfileCard } from '@/components/ProfileCard';
import { ProfileRightSidebar } from '@/components/ProfileRightSidebar';
import { PageHeader } from '@/components/PageHeader';
import { IntroImage } from '@/components/IntroImage';
import { HelpTip } from '@/components/HelpTip';
import { ImageCropDialog } from '@/components/ImageCropDialog';
import { SortableList, SortableItem } from '@/components/SortableList';
@@ -686,9 +685,14 @@ export function ProfileSettings() {
backTo="/settings"
alwaysShowBack
titleContent={
<div className="flex-1 min-w-0">
<h1 className="text-xl font-bold leading-tight">Profile</h1>
<p className="text-sm text-muted-foreground">Your Nostr identity is portable it goes wherever you go.</p>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-center size-11 rounded-xl shrink-0 bg-primary/10">
<User className="size-5 text-primary" />
</div>
<div className="min-w-0">
<h1 className="text-xl font-bold leading-tight">Profile</h1>
<p className="text-sm text-muted-foreground mt-0.5 leading-snug">Edit your display name, bio, and avatar.</p>
</div>
</div>
}
>
@@ -700,17 +704,6 @@ export function ProfileSettings() {
<Form {...form}>
<form id="profile-settings-form" onSubmit={form.handleSubmit(onSubmit)} className="max-w-xl mx-auto px-4 pb-10 space-y-6">
{/* Intro */}
<div className="flex items-center gap-4 px-3 pt-2 pb-2">
<IntroImage src="/profile-intro.png" />
<div className="min-w-0">
<h2 className="text-sm font-semibold">Your Identity</h2>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
Tap any field on the card to edit. Click your avatar or banner to upload and crop a new image.
</p>
</div>
</div>
{/* Interactive profile card */}
<ProfileCard
pubkey={user.pubkey}
+58 -30
View File
@@ -1,6 +1,18 @@
import { useSeoMeta } from '@unhead/react';
import { lazy, Suspense, useState, useEffect, useRef } from 'react';
import { ChevronRight, Settings } from 'lucide-react';
import { lazy, Suspense, useState, useEffect, useRef, type ComponentType } from 'react';
import {
Bell,
ChevronRight,
Crown,
Filter,
Palette,
Radio,
Settings,
Settings2,
Sparkles,
User,
LayoutList,
} from 'lucide-react';
import { Link, useNavigate } from 'react-router-dom';
import { PageHeader } from '@/components/PageHeader';
import { useCurrentUser } from '@/hooks/useCurrentUser';
@@ -9,6 +21,7 @@ import { IntroImage } from '@/components/IntroImage';
import { useLayoutOptions } from '@/contexts/LayoutContext';
import { toast } from '@/hooks/useToast';
import { isAdmin } from '@/lib/admins';
import { cn } from '@/lib/utils';
const RequestToVanishDialog = lazy(() => import('@/components/RequestToVanishDialog').then(m => ({ default: m.RequestToVanishDialog })));
@@ -16,6 +29,11 @@ interface SettingsSection {
id: string;
label: string;
description: string;
icon: ComponentType<{ className?: string }>;
/** Colour applied to the icon circle background (Tailwind bg-* class). */
iconBg: string;
/** Colour applied to the icon itself (Tailwind text-* class). */
iconColor: string;
illustration?: string;
path: string;
requiresAuth?: boolean;
@@ -28,6 +46,9 @@ const settingsSections: SettingsSection[] = [
id: 'profile',
label: 'Profile',
description: 'Edit your display name, bio, and avatar',
icon: User,
iconBg: 'bg-primary/10',
iconColor: 'text-primary',
illustration: '/profile-intro.png',
path: '/settings/profile',
requiresAuth: true,
@@ -35,21 +56,30 @@ const settingsSections: SettingsSection[] = [
{
id: 'appearance',
label: 'Appearance',
description: 'Switch between system, light, and dark mode',
description: 'Switch between light, dark, and system themes',
icon: Palette,
iconBg: 'bg-violet-500/10',
iconColor: 'text-violet-500',
illustration: '/theme-intro.png',
path: '/settings/appearance',
},
{
id: 'feed',
label: 'Home Feed',
description: 'Choose what types of posts appear in your home feed',
description: 'Tailor your feed to the causes and content you care about',
icon: LayoutList,
iconBg: 'bg-sky-500/10',
iconColor: 'text-sky-500',
illustration: '/community-intro.png',
path: '/settings/feed',
},
{
id: 'content',
label: 'Content',
description: 'Muted users, hashtags, and sensitive content settings',
description: 'Muted accounts, hashtags, and sensitive content filters',
icon: Filter,
iconBg: 'bg-orange-500/10',
iconColor: 'text-orange-500',
illustration: '/mute-intro.png',
path: '/settings/content',
},
@@ -57,6 +87,9 @@ const settingsSections: SettingsSection[] = [
id: 'network',
label: 'Network',
description: 'Relays and file upload servers',
icon: Radio,
iconBg: 'bg-emerald-500/10',
iconColor: 'text-emerald-500',
illustration: '/relay-intro.png',
path: '/settings/network',
requiresAuth: true,
@@ -65,6 +98,9 @@ const settingsSections: SettingsSection[] = [
id: 'notifications',
label: 'Notifications',
description: 'Configure push notification preferences',
icon: Bell,
iconBg: 'bg-yellow-500/10',
iconColor: 'text-yellow-500',
illustration: '/notification-intro.png',
path: '/settings/notifications',
requiresAuth: true,
@@ -73,6 +109,9 @@ const settingsSections: SettingsSection[] = [
id: 'advanced',
label: 'Advanced',
description: 'Wallet, system, and power user settings',
icon: Settings2,
iconBg: 'bg-muted',
iconColor: 'text-muted-foreground',
illustration: '/advanced-intro.png',
path: '/settings/advanced',
},
@@ -80,6 +119,9 @@ const settingsSections: SettingsSection[] = [
id: 'magic',
label: 'Magic',
description: 'Enchanted cursor effects and mystical interface powers',
icon: Sparkles,
iconBg: 'bg-primary/10',
iconColor: 'text-primary',
illustration: '/magic-intro.png',
path: '/settings/magic',
},
@@ -87,6 +129,9 @@ const settingsSections: SettingsSection[] = [
id: 'organizers',
label: 'Organizers',
description: 'Appoint country organizers who can pin posts to country feeds',
icon: Crown,
iconBg: 'bg-amber-500/10',
iconColor: 'text-amber-500',
illustration: '/community-intro.png',
path: '/organizers',
requiresAuth: true,
@@ -144,19 +189,11 @@ export function SettingsPage() {
{/* Page header */}
<PageHeader title="Settings" icon={<Settings className="size-5" />} backTo="/" />
{/* Codex heading + exposition */}
<div className="px-7 pb-4 pt-4 text-center space-y-2.5">
<p className="text-xs text-muted-foreground leading-relaxed select-none">
Shape your identity, tune your feed, and manage how you connect to the Nostr network.<br />Everything you need to make this place feel like yours.
{/* Intro */}
<div className="px-7 pb-6 pt-4 text-center">
<p className="text-sm text-muted-foreground leading-relaxed select-none">
Manage your profile, privacy, and how Agora works for you.
</p>
<p className="text-[10px] tracking-[0.5em] uppercase text-primary/60 select-none pt-6">Codex of Configuration</p>
</div>
{/* Tome ornament */}
<div className="flex items-center gap-3 px-6 pb-5">
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-primary/40 to-primary/60" />
<span className="text-primary/50 text-xs tracking-[0.3em] select-none"></span>
<div className="h-px flex-1 bg-gradient-to-l from-transparent via-primary/40 to-primary/60" />
</div>
{/* Settings menu */}
@@ -165,13 +202,11 @@ export function SettingsPage() {
return (
<div key={section.id}>
<div
className="flex items-center gap-4 px-3 py-2 my-1 cursor-pointer rounded-xl transition-colors hover:bg-muted/60 active:bg-muted/80 group"
className="flex items-center gap-4 px-3 py-3 my-0.5 cursor-pointer rounded-xl transition-colors hover:bg-muted/60 active:bg-muted/80 group"
onClick={() => navigate(section.path)}
>
<div className="flex items-center justify-center size-20 shrink-0">
{section.illustration && (
<IntroImage src={section.illustration} size="w-22" />
)}
<div className={cn('flex items-center justify-center size-11 rounded-xl shrink-0', section.iconBg)}>
<section.icon className={cn('size-5', section.iconColor)} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold">{section.label}</p>
@@ -179,7 +214,7 @@ export function SettingsPage() {
{section.description}
</p>
</div>
<ChevronRight className="size-4 text-primary/40 shrink-0 group-hover:text-primary/70 transition-colors" strokeWidth={4} />
<ChevronRight className="size-4 text-muted-foreground/40 shrink-0 group-hover:text-primary/70 transition-colors" strokeWidth={2.5} />
</div>
{i < visibleSections.length - 1 && (
<div className="mx-6 h-px bg-primary/10" />
@@ -207,13 +242,6 @@ export function SettingsPage() {
</Suspense>
)}
{/* Bottom ornament */}
<div className="flex items-center gap-3 px-6 pt-4 pb-2">
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-primary/20 to-primary/30" />
<span className="text-primary/30 text-[10px] tracking-[0.4em] select-none"></span>
<div className="h-px flex-1 bg-gradient-to-l from-transparent via-primary/20 to-primary/30" />
</div>
{/* Version footer */}
<Link to="/changelog" className="block text-center text-[11px] text-muted-foreground/50 hover:text-muted-foreground transition-colors select-none pt-1 pb-2">
v{import.meta.env.VERSION}{import.meta.env.COMMIT_TAG ? '' : '+'} ({new Date(import.meta.env.BUILD_DATE).toLocaleDateString()})