diff --git a/src/components/InitialSyncGate.tsx b/src/components/InitialSyncGate.tsx index 3430ac1d..3256b07b 100644 --- a/src/components/InitialSyncGate.tsx +++ b/src/components/InitialSyncGate.tsx @@ -227,8 +227,14 @@ const THEMES: ThemeOption[] = [ { id: 'system', label: 'System', description: 'Matches your device', preview: '', splitPreview: true, builtinTheme: 'system' }, { id: 'dark', label: 'Dark', description: 'Deep purple dark theme', preview: 'bg-[hsl(228,20%,10%)]', builtinTheme: 'dark' }, { id: 'light', label: 'Light', description: 'Clean and bright', preview: 'bg-white border border-border', builtinTheme: 'light' }, - { id: 'black', label: 'Black', description: 'True OLED black', preview: 'bg-black', presetId: 'black' }, - { id: 'pink', label: 'Pink', description: 'Warm and playful', preview: 'bg-[hsl(330,100%,96%)]', presetId: 'pink' }, + // Generate entries from all theme presets + ...Object.entries(themePresets).map(([id, preset]) => ({ + id, + label: preset.label, + description: `${preset.emoji} ${preset.label} theme`, + preview: `bg-[hsl(${preset.tokens.background})]`, + presetId: id, + })), ]; interface ContentKind { diff --git a/src/components/MobileBottomNav.tsx b/src/components/MobileBottomNav.tsx index f8814ac6..9ef37cd0 100644 --- a/src/components/MobileBottomNav.tsx +++ b/src/components/MobileBottomNav.tsx @@ -1,6 +1,11 @@ import { useCallback, useMemo, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { Home, Compass, Bell, User, Search, TrendingUp, Bookmark, Clapperboard, BarChart3, Palette, PartyPopper, Radio, FileText } from 'lucide-react'; +import { Home, Compass, Bell, User, Search, Bookmark, TrendingUp, Clapperboard, BarChart3, Palette, PartyPopper, Radio, FileText, Pencil, GripVertical, X, Plus } from 'lucide-react'; +import LoginDialog from '@/components/auth/LoginDialog'; +import { useOnboarding } from '@/components/InitialSyncGate'; +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'; @@ -90,21 +95,95 @@ 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 { user, metadata } = useCurrentUser(); const hasUnread = useHasUnreadNotifications(); - const { orderedItems } = useFeedSettings(); + const { + orderedItems, hiddenItems, updateSidebarOrder, addToSidebar, removeFromSidebar, + } = useFeedSettings(); const userProfileUrl = useProfileUrl(user?.pubkey ?? '', metadata); + const { startSignup } = useOnboarding(); const [exploreOpen, setExploreOpen] = useState(false); + const [editing, setEditing] = useState(false); + const [loginDialogOpen, setLoginDialogOpen] = useState(false); - const handleHomeClick = useCallback(() => { - if (location.pathname === '/') { - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - }, [location.pathname]); + // 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 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(() => { @@ -116,88 +195,162 @@ export function MobileBottomNav() { })); }, [orderedItems]); - // Check if current path matches any explore route (excluding __feed which is the Home tab) + // Check if current path matches any explore route const isExploreActive = orderedItems.some((id) => - id !== '__feed' && isItemActive(id, location.pathname, location.search), + 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) - ? 'text-accent 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) + ? 'text-accent font-semibold' + : 'text-foreground hover:bg-secondary/60', + )} + > + {item.icon} + {item.label} + + ))} +
+ ) : ( +

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

+ ) )}
+ + {/* Login dialog for logged-out "You" tab */} + setLoginDialogOpen(false)} + onLogin={() => setLoginDialogOpen(false)} + onSignupClick={startSignup} + /> ); } diff --git a/src/components/MobileDrawer.tsx b/src/components/MobileDrawer.tsx index 9b007dcc..3d7706ac 100644 --- a/src/components/MobileDrawer.tsx +++ b/src/components/MobileDrawer.tsx @@ -1,144 +1,22 @@ -import { Link, useLocation, useNavigate } from 'react-router-dom'; -import { useUserThemes } from '@/hooks/useUserThemes'; -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 { Link, useNavigate } from 'react-router-dom'; +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 { useUserThemes } from '@/hooks/useUserThemes'; 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, type ThemeTokens } 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: , - __bookmarks: , - 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) ─────────────────────────────────────────── - -interface DrawerMenuItemProps { - to: string; - icon: React.ReactNode; - label: string; - onClick: () => void; -} - -function DrawerMenuItem({ to, icon, label, onClick }: DrawerMenuItemProps) { - const location = useLocation(); - const active = location.pathname === to; - - return ( - - {icon} - {label} - - ); -} - -// ── 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; @@ -150,41 +28,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 }[] = [ @@ -268,10 +118,7 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) { } }; - const handleClose = () => { - setEditing(false); - onOpenChange(false); - }; + const handleClose = () => onOpenChange(false); const handleLogout = async () => { await logout(); @@ -280,8 +127,8 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) { }; return ( - { if (!v) setEditing(false); onOpenChange(v); }}> - + + Navigation menu {user ? ( @@ -297,96 +144,32 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) { {/* Menu items */}