diff --git a/src/components/LeftSidebar.tsx b/src/components/LeftSidebar.tsx index 88f28a0d..6bfad439 100644 --- a/src/components/LeftSidebar.tsx +++ b/src/components/LeftSidebar.tsx @@ -48,6 +48,7 @@ const ITEM_ICONS: Record = { // Built-ins __feed: , __trends: , + __bookmarks: , // Extra-kind routes vines: , polls: , @@ -427,12 +428,6 @@ export function LeftSidebar() { label="Profile" active={location.pathname === userProfileUrl} /> - } - label="Bookmarks" - active={location.pathname === '/bookmarks'} - /> } diff --git a/src/components/MobileBottomNav.tsx b/src/components/MobileBottomNav.tsx index 421962cb..428b1983 100644 --- a/src/components/MobileBottomNav.tsx +++ b/src/components/MobileBottomNav.tsx @@ -1,6 +1,9 @@ import { useCallback, useMemo, useState } from 'react'; -import { Link, useLocation, useNavigate } from 'react-router-dom'; -import { Home, Compass, Bell, User, Search, TrendingUp, Clapperboard, BarChart3, Palette, PartyPopper, Radio, FileText } from 'lucide-react'; +import { Link, useLocation } from 'react-router-dom'; +import { Home, Compass, Bell, User, Search, Bookmark, TrendingUp, Clapperboard, BarChart3, Palette, PartyPopper, Radio, FileText, Pencil, GripVertical, X, Plus } from 'lucide-react'; +import { DndContext, closestCenter, TouchSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'; +import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; import { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/drawer'; import { ChestIcon } from '@/components/icons/ChestIcon'; import { CardsIcon } from '@/components/icons/CardsIcon'; @@ -16,6 +19,7 @@ import { useProfileUrl } from '@/hooks/useProfileUrl'; const ITEM_ICONS: Record = { __feed: , __trends: , + __bookmarks: , vines: , polls: , treasures: , @@ -41,6 +45,7 @@ function itemPath(id: string): string { function isItemActive(id: string, pathname: string, search: string): boolean { if (id === '__feed') return pathname === '/'; if (id === '__trends') return pathname === '/search' && search.includes('tab=trends'); + if (id === '__bookmarks') return pathname === '/bookmarks'; return pathname === `/${id}`; } @@ -88,16 +93,81 @@ function NavTab({ icon, label, active, showIndicator, onClick, to }: NavTabProps ); } +// ── Sortable explore item (edit mode) ───────────────────────────────────────── + +interface SortableExploreSheetItemProps { + id: string; + onRemove: (id: string) => void; +} + +function SortableExploreSheetItem({ id, onRemove }: SortableExploreSheetItemProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + const icon = ITEM_ICONS[id] ?? ; + const label = itemLabel(id); + + return ( +
+ + +
+ {icon} + {label} +
+ + +
+ ); +} + // ── Main component ──────────────────────────────────────────────────────────── export function MobileBottomNav() { const location = useLocation(); - const navigate = useNavigate(); const { user, metadata } = useCurrentUser(); const hasUnread = useHasUnreadNotifications(); - const { orderedItems } = useFeedSettings(); + const { + orderedItems, hiddenItems, updateSidebarOrder, addToSidebar, removeFromSidebar, + } = useFeedSettings(); const userProfileUrl = useProfileUrl(user?.pubkey ?? '', metadata); const [exploreOpen, setExploreOpen] = useState(false); + const [editing, setEditing] = useState(false); + + // DnD sensors — touch sensor with delay to distinguish scroll from drag + const sensors = useSensors( + useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5 } }), + useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), + ); const handleHomeClick = useCallback(() => { if (location.pathname === '/') { @@ -105,6 +175,18 @@ export function MobileBottomNav() { } }, [location.pathname]); + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const oldIndex = orderedItems.indexOf(active.id as string); + const newIndex = orderedItems.indexOf(over.id as string); + if (oldIndex === -1 || newIndex === -1) return; + + const newOrder = arrayMove(orderedItems, oldIndex, newIndex); + updateSidebarOrder(newOrder); + }, [orderedItems, updateSidebarOrder]); + // Build explore items from ordered items (includes built-ins) const exploreItems = useMemo(() => { return orderedItems.map((id) => ({ @@ -120,6 +202,11 @@ export function MobileBottomNav() { id !== '__feed' && isItemActive(id, location.pathname, location.search), ); + const handleDrawerClose = (open: boolean) => { + if (!open) setEditing(false); + setExploreOpen(open); + }; + return ( <> {/* Explore bottom sheet */} - + Explore
-

- Explore -

- {exploreItems.length > 0 ? ( -
- {exploreItems.map((item) => ( - setExploreOpen(false)} - className={cn( - 'flex items-center gap-3 px-4 py-3.5 rounded-xl transition-colors', - isItemActive(item.id, location.pathname, location.search) - ? 'bg-primary/10 text-primary font-semibold' - : 'text-foreground hover:bg-secondary/60', - )} + {/* Section header with edit toggle */} +
+ + Explore + +
+ +
+ + {editing ? ( + /* ── Edit mode: single-column sortable list ── */ + <> + + - {item.icon} - {item.label} - - ))} -
+ {orderedItems.map((id) => ( + + ))} + + + + {/* Add hidden items back */} + {hiddenItems.length > 0 && ( +
+ {hiddenItems.map((item) => ( + + ))} +
+ )} + ) : ( -

- No content sections enabled. Go to Settings to add some. -

+ /* ── Normal mode: 2-column grid of links ── */ + exploreItems.length > 0 ? ( +
+ {exploreItems.map((item) => ( + setExploreOpen(false)} + className={cn( + 'flex items-center gap-3 px-4 py-3.5 rounded-xl transition-colors', + isItemActive(item.id, location.pathname, location.search) + ? 'bg-primary/10 text-primary font-semibold' + : 'text-foreground hover:bg-secondary/60', + )} + > + {item.icon} + {item.label} + + ))} +
+ ) : ( +

+ No content sections enabled. Tap the pencil to add some. +

+ ) )}
diff --git a/src/components/MobileDrawer.tsx b/src/components/MobileDrawer.tsx index 6cddea2f..1383f317 100644 --- a/src/components/MobileDrawer.tsx +++ b/src/components/MobileDrawer.tsx @@ -1,54 +1,21 @@ import { Link, useNavigate } from 'react-router-dom'; -import { Home, TrendingUp, Bookmark, Settings, LogOut, ChevronDown, ChevronUp, Sun, Moon, Monitor, Clapperboard, BarChart3, Palette, PartyPopper, Radio, FileText, Pencil, GripVertical, X, Plus } from 'lucide-react'; -import { DndContext, closestCenter, TouchSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'; -import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; +import { LogOut, ChevronDown, ChevronUp, Sun, Moon, Monitor, Palette } from 'lucide-react'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Sheet, SheetContent, SheetTitle } from '@/components/ui/sheet'; import { Separator } from '@/components/ui/separator'; -import { ChestIcon } from '@/components/icons/ChestIcon'; -import { CardsIcon } from '@/components/icons/CardsIcon'; import { DittoLogo } from '@/components/DittoLogo'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useLoginActions } from '@/hooks/useLoginActions'; import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts'; import { useTheme } from '@/hooks/useTheme'; -import { useFeedSettings, getBuiltinItem } from '@/hooks/useFeedSettings'; -import { EXTRA_KINDS } from '@/lib/extraKinds'; import { LoginArea } from '@/components/auth/LoginArea'; import { genUserName } from '@/lib/genUserName'; -import { useCallback, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import type { Theme } from '@/contexts/AppContext'; import { themePresets } from '@/themes'; -import { cn } from '@/lib/utils'; +import { settingsSections, type SettingsSection } from '@/pages/SettingsPage'; -/** Map item ID to icon for drawer items. Covers both built-ins and extra-kind routes. */ -const ITEM_ICONS: Record = { - __feed: , - __trends: , - vines: , - polls: , - treasures: , - colors: , - packs: , - streams: , - articles: , - decks: , -}; - -function itemLabel(id: string): string { - const builtin = getBuiltinItem(id); - if (builtin) return builtin.label; - return EXTRA_KINDS.find((d) => d.route === id)?.label ?? id; -} - -function itemPath(id: string): string { - const builtin = getBuiltinItem(id); - if (builtin) return builtin.path; - return `/${id}`; -} - -// ── Drawer menu item (normal mode) ─────────────────────────────────────────── +// ── Drawer menu item ───────────────────────────────────────────────────────── interface DrawerMenuItemProps { to: string; @@ -70,67 +37,7 @@ function DrawerMenuItem({ to, icon, label, onClick }: DrawerMenuItemProps) { ); } -// ── Sortable drawer item (edit mode) ───────────────────────────────────────── - -interface SortableDrawerItemProps { - id: string; - onRemove: (id: string) => void; -} - -function SortableDrawerItem({ id, onRemove }: SortableDrawerItemProps) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; - - const icon = ITEM_ICONS[id] ?? ; - const label = itemLabel(id); - - return ( -
- {/* Drag handle */} - - - {/* Icon + label (non-interactive in edit mode) */} -
- {icon} - {label} -
- - {/* Remove button */} - -
- ); -} - -// ── Mobile drawer props ────────────────────────────────────────────────────── +// ── Mobile drawer ──────────────────────────────────────────────────────────── interface MobileDrawerProps { open: boolean; @@ -142,41 +49,13 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) { const { logout } = useLoginActions(); const { otherUsers, setLogin } = useLoggedInAccounts(); const { theme, setTheme, applyCustomTheme, customTheme } = useTheme(); - const { - orderedItems, hiddenItems, updateSidebarOrder, addToSidebar, removeFromSidebar, - } = useFeedSettings(); const navigate = useNavigate(); const [showAccountSwitcher, setShowAccountSwitcher] = useState(false); - const [editing, setEditing] = useState(false); - // DnD sensors — touch sensor with delay to distinguish scroll from drag - const sensors = useSensors( - useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5 } }), - useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), - ); - - /** Build explore items from ordered items (includes built-ins). */ - const exploreItems = useMemo(() => { - return orderedItems.map((id) => ({ - id, - to: itemPath(id), - icon: ITEM_ICONS[id] ?? , - label: itemLabel(id), - })); - }, [orderedItems]); - - /** Handle drag-and-drop reorder. */ - const handleDragEnd = useCallback((event: DragEndEvent) => { - const { active, over } = event; - if (!over || active.id === over.id) return; - - const oldIndex = orderedItems.indexOf(active.id as string); - const newIndex = orderedItems.indexOf(over.id as string); - if (oldIndex === -1 || newIndex === -1) return; - - const newOrder = arrayMove(orderedItems, oldIndex, newIndex); - updateSidebarOrder(newOrder); - }, [orderedItems, updateSidebarOrder]); + /** Settings sections filtered by auth state. */ + const visibleSettings = useMemo(() => { + return settingsSections.filter((s) => !s.requiresAuth || user); + }, [user]); // Theme cycling logic const builtinCycle: { id: Theme; label: string; icon: React.ReactNode }[] = [ @@ -225,10 +104,7 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) { } }; - const handleClose = () => { - setEditing(false); - onOpenChange(false); - }; + const handleClose = () => onOpenChange(false); const handleLogout = async () => { await logout(); @@ -237,7 +113,7 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) { }; return ( - { if (!v) setEditing(false); onOpenChange(v); }}> + Navigation menu @@ -254,102 +130,30 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) { {/* Menu items */}