pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add settings search bar

Pas d756108c 8c6d5031

+138 -4
+3
src/assets/locales/en.json
··· 882 882 } 883 883 }, 884 884 "settings": { 885 + "search": { 886 + "placeholder": "Search settings..." 887 + }, 885 888 "account": { 886 889 "accountDetails": { 887 890 "deviceNameLabel": "Device name",
+135 -4
src/pages/Settings.tsx
··· 1 1 import classNames from "classnames"; 2 - import { useCallback, useEffect, useMemo } from "react"; 2 + import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 3 3 import { useTranslation } from "react-i18next"; 4 - import { useAsyncFn } from "react-use"; 4 + import { useAsyncFn, useWindowSize } from "react-use"; 5 5 6 6 import { 7 7 base64ToBuffer, ··· 13 13 import { editUser } from "@/backend/accounts/user"; 14 14 import { getAllProviders } from "@/backend/providers/providers"; 15 15 import { Button } from "@/components/buttons/Button"; 16 + import { SearchBarInput } from "@/components/form/SearchBar"; 17 + import { ThinContainer } from "@/components/layout/ThinContainer"; 16 18 import { WideContainer } from "@/components/layout/WideContainer"; 17 19 import { UserIcons } from "@/components/UserIcon"; 18 20 import { Heading1 } from "@/components/utils/Text"; ··· 39 41 import { SubPageLayout } from "./layouts/SubPageLayout"; 40 42 import { PreferencesPart } from "./parts/settings/PreferencesPart"; 41 43 42 - function SettingsLayout(props: { children: React.ReactNode }) { 44 + function SettingsLayout(props: { 45 + children: React.ReactNode; 46 + searchQuery: string; 47 + onSearchChange: (value: string, force: boolean) => void; 48 + onSearchUnFocus: (newSearch?: string) => void; 49 + }) { 50 + const { t } = useTranslation(); 43 51 const { isMobile } = useIsMobile(); 52 + const searchRef = useRef<HTMLInputElement>(null); 53 + const { width: windowWidth, height: windowHeight } = useWindowSize(); 54 + 55 + // Dynamic offset calculation like HeroPart 56 + const topSpacing = 16; // Base spacing 57 + const [stickyOffset, setStickyOffset] = useState(topSpacing); 58 + 59 + // Detect if running as a PWA on iOS 60 + const isIOSPWA = 61 + /iPad|iPhone|iPod/i.test(navigator.userAgent) && 62 + window.matchMedia("(display-mode: standalone)").matches; 63 + 64 + const adjustedTopSpacing = isIOSPWA ? 60 : topSpacing; 65 + const isLandscape = windowHeight < windowWidth && isIOSPWA; 66 + const adjustedOffset = isLandscape ? -40 : 0; 67 + 68 + useEffect(() => { 69 + if (windowWidth > 1280) { 70 + // On large screens the bar goes inline with the nav elements 71 + setStickyOffset(adjustedTopSpacing); 72 + } else { 73 + // On smaller screens the bar goes below the nav elements 74 + setStickyOffset(adjustedTopSpacing + 60 + adjustedOffset); 75 + } 76 + }, [adjustedOffset, adjustedTopSpacing, windowWidth]); 44 77 45 78 return ( 46 79 <WideContainer ultraWide classNames="overflow-visible"> 80 + {/* Floating Search Bar - starts in sticky state */} 81 + <div 82 + className="fixed left-0 right-0 z-[500]" 83 + style={{ 84 + top: `${stickyOffset}px`, 85 + }} 86 + > 87 + <ThinContainer> 88 + <SearchBarInput 89 + ref={searchRef} 90 + onChange={props.onSearchChange} 91 + value={props.searchQuery} 92 + onUnFocus={props.onSearchUnFocus} 93 + placeholder={t("settings.search.placeholder")} 94 + isSticky 95 + hideTooltip 96 + /> 97 + </ThinContainer> 98 + </div> 99 + 47 100 <div 48 101 className={classNames( 49 102 "grid gap-12", 50 103 isMobile ? "grid-cols-1" : "lg:grid-cols-[280px,1fr]", 51 104 )} 105 + data-settings-content 52 106 > 53 107 <SidebarPart /> 54 108 <div>{props.children}</div> ··· 102 156 } 103 157 104 158 export function SettingsPage() { 159 + const [searchQuery, setSearchQuery] = useState(""); 160 + 105 161 useEffect(() => { 106 162 const hash = window.location.hash; 107 163 if (hash) { ··· 117 173 const setTheme = useThemeStore((s) => s.setTheme); 118 174 const previewTheme = usePreviewThemeStore((s) => s.previewTheme); 119 175 const setPreviewTheme = usePreviewThemeStore((s) => s.setPreviewTheme); 176 + 177 + // Simple text search with highlighting 178 + const handleSearchChange = useCallback((value: string, _force: boolean) => { 179 + setSearchQuery(value); 180 + 181 + // Remove existing highlights 182 + const existingHighlights = document.querySelectorAll(".search-highlight"); 183 + existingHighlights.forEach((el) => { 184 + const parent = el.parentNode; 185 + if (parent) { 186 + parent.replaceChild(document.createTextNode(el.textContent || ""), el); 187 + parent.normalize(); 188 + } 189 + }); 190 + 191 + if (value.trim()) { 192 + // Find and highlight matching text 193 + const walker = document.createTreeWalker( 194 + document.querySelector("[data-settings-content]") || document.body, 195 + NodeFilter.SHOW_TEXT, 196 + null, 197 + ); 198 + 199 + let node = walker.nextNode(); 200 + 201 + while (node) { 202 + const text = node.textContent || ""; 203 + const lowerText = text.toLowerCase(); 204 + const lowerValue = value.toLowerCase(); 205 + 206 + if (lowerText.includes(lowerValue)) { 207 + const regex = new RegExp( 208 + `(${value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, 209 + "gi", 210 + ); 211 + const highlightedText = text.replace( 212 + regex, 213 + '<span class="search-highlight bg-yellow-200 text-black px-1 rounded">$1</span>', 214 + ); 215 + 216 + if (highlightedText !== text) { 217 + const wrapper = document.createElement("div"); 218 + wrapper.innerHTML = highlightedText; 219 + const parent = node.parentNode; 220 + if (parent) { 221 + while (wrapper.firstChild) { 222 + parent.insertBefore(wrapper.firstChild, node); 223 + } 224 + parent.removeChild(node); 225 + } 226 + } 227 + } 228 + node = walker.nextNode(); 229 + } 230 + 231 + // Scroll to first highlighted element 232 + const firstHighlighted = document.querySelector(".search-highlight"); 233 + if (firstHighlighted) { 234 + firstHighlighted.scrollIntoView({ 235 + behavior: "smooth", 236 + block: "center", 237 + }); 238 + } 239 + } 240 + }, []); 241 + 242 + const handleSearchUnFocus = useCallback((newSearch?: string) => { 243 + if (newSearch !== undefined) { 244 + setSearchQuery(newSearch); 245 + } 246 + }, []); 120 247 121 248 const appLanguage = useLanguageStore((s) => s.language); 122 249 const setAppLanguage = useLanguageStore((s) => s.setLanguage); ··· 476 603 return ( 477 604 <SubPageLayout> 478 605 <PageTitle subpage k="global.pages.settings" /> 479 - <SettingsLayout> 606 + <SettingsLayout 607 + searchQuery={searchQuery} 608 + onSearchChange={handleSearchChange} 609 + onSearchUnFocus={handleSearchUnFocus} 610 + > 480 611 <div id="settings-account"> 481 612 <Heading1 border className="!mb-0"> 482 613 {t("settings.account.title")}