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;