From e0e236bd26dad3a529492b921cbfb7edeaad56d4 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun <cheeaun@gmail.com> Date: Fri, 10 Feb 2023 22:10:13 +0800 Subject: [PATCH] Experimental hidden search page And refactored out some reusable components --- src/app.css | 15 +++--- src/app.jsx | 3 +- src/components/menu.jsx | 55 +++++++++++++++++++ src/components/timeline.jsx | 48 ++--------------- src/pages/notifications.jsx | 6 +++ src/pages/search.css | 29 ++++++++++ src/pages/search.jsx | 104 ++++++++++++++++++++++++++++++++++++ 7 files changed, 208 insertions(+), 52 deletions(-) create mode 100644 src/components/menu.jsx create mode 100644 src/pages/search.css create mode 100644 src/pages/search.jsx diff --git a/src/app.css b/src/app.css index bad39b57..d51c0417 100644 --- a/src/app.css +++ b/src/app.css @@ -106,8 +106,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { border-bottom: var(--hairline-width) solid var(--divider-color); min-height: 3em; display: grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: 1fr 2fr 1fr; align-items: center; + text-overflow: ellipsis; + white-space: nowrap; } .deck > header .header-grid > .header-side:last-of-type { text-align: right; @@ -121,7 +123,6 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { padding: 0; font-size: 1.2em; text-align: center; - white-space: nowrap; } .deck > header .header-grid h1:first-child { text-align: left; @@ -1252,7 +1253,7 @@ meter.donut:is(.danger, .explode):after { .updates-button { margin-top: 24px; } - .timeline-deck .timeline:not(.flat) > li { + .timeline:not(.flat) > li { border: 1px solid var(--divider-color); margin: 16px 0; background-color: var(--bg-color); @@ -1262,15 +1263,13 @@ meter.donut:is(.danger, .explode):after { transition: transform 0.4s var(--timing-function); --back-transition: transform 0.4s ease-out; } - .timeline-deck .timeline:not(.flat) > li:has(.status-link.is-active) { + .timeline:not(.flat) > li:has(.status-link.is-active) { transition: var(--back-transition); transform: translate3d(-2.5vw, 0, 0); } - .timeline-deck - .timeline:not(.flat) + .timeline:not(.flat) > li:not(:has(.boost-carousel)):has(+ li .status-link.is-active), - .timeline-deck - .timeline:not(.flat) + .timeline:not(.flat) > li:not(:has(.boost-carousel)):has(.status-link.is-active) + li { transition: var(--back-transition); diff --git a/src/app.jsx b/src/app.jsx index 602567bf..f43c470e 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -39,6 +39,7 @@ import Lists from './pages/lists'; import Login from './pages/login'; import Notifications from './pages/notifications'; import Public from './pages/public'; +import Search from './pages/search'; import Settings from './pages/settings'; import Status from './pages/status'; import Welcome from './pages/welcome'; @@ -215,7 +216,7 @@ function App() { <Route index element={<Public />} /> <Route path="l" element={<Public local />} /> </Route> - {/* <Route path="/:instance?/p/l?" element={<Public />} /> */} + <Route path="/:instance?/search" element={<Search />} /> {/* <Route path="/:anything" element={<NotFound />} /> */} </Routes> <Routes> diff --git a/src/components/menu.jsx b/src/components/menu.jsx new file mode 100644 index 00000000..5b7508ff --- /dev/null +++ b/src/components/menu.jsx @@ -0,0 +1,55 @@ +import { FocusableItem, Menu, MenuDivider, MenuItem } from '@szhsin/react-menu'; + +import states from '../utils/states'; + +import Icon from './icon'; +import Link from './link'; + +function NavMenu(props) { + return ( + <Menu + {...props} + menuButton={ + <button type="button" class="button plain"> + <Icon icon="menu" size="l" /> + </button> + } + > + <MenuLink to="/"> + <Icon icon="home" size="l" /> <span>Home</span> + </MenuLink> + <MenuLink to="/b"> + <Icon icon="bookmark" size="l" /> <span>Bookmarks</span> + </MenuLink> + <MenuLink to="/f"> + <Icon icon="heart" size="l" /> <span>Favourites</span> + </MenuLink> + <MenuDivider /> + <MenuItem + onClick={() => { + states.showSettings = true; + }} + > + <Icon icon="gear" size="l" alt="Settings" /> <span>Settings</span> + </MenuItem> + </Menu> + ); +} + +function MenuLink(props) { + return ( + <FocusableItem> + {({ ref, closeMenu }) => ( + <Link + {...props} + ref={ref} + onClick={({ detail }) => + closeMenu(detail === 0 ? 'Enter' : undefined) + } + /> + )} + </FocusableItem> + ); +} + +export default NavMenu; diff --git a/src/components/timeline.jsx b/src/components/timeline.jsx index b5a5c835..b5c88c8b 100644 --- a/src/components/timeline.jsx +++ b/src/components/timeline.jsx @@ -1,9 +1,7 @@ -import { FocusableItem, Menu, MenuDivider, MenuItem } from '@szhsin/react-menu'; import { useEffect, useRef, useState } from 'preact/hooks'; import { useHotkeys } from 'react-hotkeys-hook'; import { useDebouncedCallback } from 'use-debounce'; -import states from '../utils/states'; import useInterval from '../utils/useInterval'; import usePageVisibility from '../utils/usePageVisibility'; import useScroll from '../utils/useScroll'; @@ -11,6 +9,7 @@ import useScroll from '../utils/useScroll'; import Icon from './icon'; import Link from './link'; import Loader from './loader'; +import Menu from './menu'; import Status from './status'; function Timeline({ @@ -257,31 +256,10 @@ function Timeline({ <div class="header-grid"> <div class="header-side"> <Menu - menuButton={ - <button type="button" class="button plain"> - <Icon icon="menu" size="l" /> - </button> - } - > - <MenuLink to="/"> - <Icon icon="home" size="l" /> <span>Home</span> - </MenuLink> - <MenuLink to="/b"> - <Icon icon="bookmark" size="l" /> <span>Bookmarks</span> - </MenuLink> - <MenuLink to="/f"> - <Icon icon="heart" size="l" /> <span>Favourites</span> - </MenuLink> - <MenuDivider /> - <MenuItem - onClick={() => { - states.showSettings = true; - }} - > - <Icon icon="gear" size="l" alt="Settings" />{' '} - <span>Settings</span> - </MenuItem> - </Menu> + portal={{ + target: scrollableRef.current, + }} + /> {headerStart !== null && headerStart !== undefined ? ( headerStart ) : ( @@ -409,22 +387,6 @@ function Timeline({ ); } -function MenuLink(props) { - return ( - <FocusableItem> - {({ ref, closeMenu }) => ( - <Link - {...props} - ref={ref} - onClick={({ detail }) => - closeMenu(detail === 0 ? 'Enter' : undefined) - } - /> - )} - </FocusableItem> - ); -} - function groupBoosts(values) { let newValues = []; let boostStash = []; diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx index 8a7c01ab..4e27f943 100644 --- a/src/pages/notifications.jsx +++ b/src/pages/notifications.jsx @@ -8,6 +8,7 @@ import Avatar from '../components/avatar'; import Icon from '../components/icon'; import Link from '../components/link'; import Loader from '../components/loader'; +import Menu from '../components/menu'; import NameText from '../components/name-text'; import RelativeTime from '../components/relative-time'; import Status from '../components/status'; @@ -145,6 +146,11 @@ function Notifications() { > <div class="header-grid"> <div class="header-side"> + <Menu + portal={{ + target: scrollableRef.current, + }} + /> <Link to="/" class="button plain"> <Icon icon="home" size="l" /> </Link> diff --git a/src/pages/search.css b/src/pages/search.css new file mode 100644 index 00000000..3580eb90 --- /dev/null +++ b/src/pages/search.css @@ -0,0 +1,29 @@ +#search-page header input { + width: 100%; + padding: 8px 16px; + border: 0; + border-radius: 999px; + background-color: var(--bg-faded-color); +} +#search-page header input:focus { + background-color: var(--bg-color); + outline: 2px solid var(--link-color); +} + +#search-page ul.accounts-list { + display: flex; + flex-wrap: wrap; +} +#search-page ul.accounts-list li { + flex-basis: 320px; + display: flex; + padding: 8px 16px; + gap: 8px; + align-items: center; +} + +@media (min-width: 40em) { + #search-page header input { + background-color: var(--bg-color); + } +} diff --git a/src/pages/search.jsx b/src/pages/search.jsx new file mode 100644 index 00000000..05467cf0 --- /dev/null +++ b/src/pages/search.jsx @@ -0,0 +1,104 @@ +import './search.css'; + +import { useEffect, useRef, useState } from 'preact/hooks'; +import { useSearchParams } from 'react-router-dom'; + +import Avatar from '../components/avatar'; +import Link from '../components/link'; +import Menu from '../components/menu'; +import NameText from '../components/name-text'; +import Status from '../components/status'; +import { api } from '../utils/api'; + +function Search() { + const { masto, instance, authenticated } = api(); + const [searchParams, setSearchParams] = useSearchParams(); + const searchFieldRef = useRef(); + const q = searchParams.get('q'); + const [statusResults, setStatusResults] = useState([]); + const [accountResults, setAccountResults] = useState([]); + useEffect(() => { + if (q) { + searchFieldRef.current.value = q; + + (async () => { + const results = await masto.v2.search({ + q, + limit: 20, + resolve: authenticated, + }); + console.log(results); + setStatusResults(results.statuses); + setAccountResults(results.accounts); + })(); + } + }, [q]); + + console.log({ accountResults }); + + return ( + <div id="search-page" class="deck-container"> + <div class="timeline-deck deck"> + <header> + <div class="header-grid"> + <div class="header-side"> + <Menu /> + </div> + <form + onSubmit={(e) => { + e.preventDefault(); + const { q } = e.target; + if (q.value) { + setSearchParams({ q: q.value }); + } + }} + > + <input + ref={searchFieldRef} + name="q" + type="search" + autofocus + placeholder="Search or paste URL" + /> + </form> + <div class="header-side" /> + </div> + </header> + <main> + <h2 class="timeline-header">Accounts</h2> + {accountResults.length > 0 && ( + <ul class="timeline flat accounts-list"> + {accountResults.map((account) => ( + <li> + <Avatar url={account.avatar} size="xl" /> + <NameText account={account} instance={instance} showAcct /> + </li> + ))} + </ul> + )} + <h2 class="timeline-header">Posts</h2> + {statusResults.length > 0 && ( + <ul class="timeline"> + {statusResults.map((status) => ( + <li> + <Link + class="status-link" + to={ + instance + ? `/${instance}/s/${status.id}` + : `/s/${status.id}` + } + > + <Status status={status} /> + </Link> + </li> + ))} + </ul> + )} + </main> + </div> + </div> + ); +} + +export default Search;