From b77265e6b69490ded76f9f82c86b3a95e4cecd39 Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Thu, 24 Mar 2022 15:35:59 -0700 Subject: [PATCH] client: add client-side search of posts list --- client/components/post-list/index.tsx | 87 ++++++++++++++++--- client/components/post-list/list-item.tsx | 3 +- .../components/post-list/post-list.module.css | 39 +++++---- client/styles/globals.css | 8 +- 4 files changed, 105 insertions(+), 32 deletions(-) diff --git a/client/components/post-list/index.tsx b/client/components/post-list/index.tsx index 915d7720..49704b51 100644 --- a/client/components/post-list/index.tsx +++ b/client/components/post-list/index.tsx @@ -1,33 +1,94 @@ -import { Text } from "@geist-ui/core" +import { Code, Dot, Input, Note, Text } from "@geist-ui/core" import NextLink from "next/link" import Link from '../Link' import styles from './post-list.module.css' import ListItemSkeleton from "./list-item-skeleton" import ListItem from "./list-item" +import { Post } from "@lib/types" +import { ChangeEvent, useEffect, useMemo, useState } from "react" +import debounce from "lodash.debounce" type Props = { - posts: any + posts: Post[] error: any } const PostList = ({ posts, error }: Props) => { + const [search, setSearchValue] = useState('') + // const [searching, setSearching] = useState(false) + const [searchResults, setSearchResults] = useState(posts) + + // update posts on search + useEffect(() => { + if (search) { + // support filters like "title is:private has:content" in the text + // extract the filters + const filters = search.split(' ').filter(s => s.includes(':')) + const filtersMap = new Map() + filters.forEach(f => { + const [key, value] = f.split(':') + filtersMap.set(key, value) + }) + + const results = posts.filter(post => { + if (filtersMap.has('is') && filtersMap.get('is') !== post.visibility) { + return false + } + for (const file of post.files) { + if (file.content.toLowerCase().includes(search.toLowerCase())) { + return true + } + } + return post.title.toLowerCase().includes(search.toLowerCase()) + }) + setSearchResults(results) + + } else { + setSearchResults(posts) + } + }, [search, posts]) + + const handleSearchChange = (e: ChangeEvent) => { + setSearchValue(e.target.value) + } + + const debouncedSearchHandler = useMemo( + () => debounce(handleSearchChange, 300) + , []); + + useEffect(() => { + return () => { + debouncedSearchHandler.cancel(); + } + }, [debouncedSearchHandler]); + return (
+
+ + Available filters: is:visibility +
{error && Failed to load.} - {!posts &&
    -
  • - -
  • -
  • - -
  • -
} - {posts?.length === 0 && You have no posts. Create one here.} { - posts?.length > 0 &&
+ !posts &&
    +
  • + +
  • +
  • + +
  • +
+ } + {posts.length === 0 && !error && No posts found.Create one here.} + {searchResults.length === 0 && No posts returned. Create one here.} + { + searchResults?.length > 0 &&
    - {posts.map((post: any) => { + {searchResults.map((post) => { return })}
diff --git a/client/components/post-list/list-item.tsx b/client/components/post-list/list-item.tsx index 2eef160b..f9f3f40e 100644 --- a/client/components/post-list/list-item.tsx +++ b/client/components/post-list/list-item.tsx @@ -8,13 +8,14 @@ import getPostPath from "@lib/get-post-path" import { Input, Link, Text, Card, Spacer, Grid, Tooltip, Divider } from "@geist-ui/core" const FilenameInput = ({ title }: { title: string }) => const ListItem = ({ post }: { post: any }) => { diff --git a/client/components/post-list/post-list.module.css b/client/components/post-list/post-list.module.css index de6d8d14..b583bbc9 100644 --- a/client/components/post-list/post-list.module.css +++ b/client/components/post-list/post-list.module.css @@ -1,26 +1,35 @@ .container ul { - list-style: none; - padding: 0; - margin: 0; + list-style: none; + padding: 0; + margin: 0; } .container ul li { - padding: 0.5rem 0; + padding: 0.5rem 0; } .container ul li::before { - content: ""; - padding: 0; - margin: 0; + content: ""; + padding: 0; + margin: 0; } .postHeader { - display: flex; - justify-content: space-between; - padding: var(--gap); - align-items: center; - position: sticky; - top: 0; - z-index: 1; - background: inherit; + display: flex; + justify-content: space-between; + padding: var(--gap); + align-items: center; + position: sticky; + top: 0; + z-index: 1; + background: inherit; +} + +.searchContainer { + display: flex; + align-items: center; + flex-direction: column-reverse; + justify-content: center; + margin-top: var(--gap); + margin-bottom: var(--gap-double); } diff --git a/client/styles/globals.css b/client/styles/globals.css index 78986236..6a04b2a8 100644 --- a/client/styles/globals.css +++ b/client/styles/globals.css @@ -8,15 +8,17 @@ --gap-half: 0.5rem; --gap: 1rem; --gap-double: 2rem; - --gap-negative: calc(-1 * var(--gap)); - --gap-half-negative: calc(-1 * var(--gap-half)); - --gap-quarter-negative: calc(-1 * var(--gap-quarter)); + --small-gap: 4rem; --big-gap: 4rem; --main-content: 55rem; --radius: 8px; --inline-radius: 5px; + --gap-negative: calc(-1 * var(--gap)); + --gap-half-negative: calc(-1 * var(--gap-half)); + --gap-quarter-negative: calc(-1 * var(--gap-quarter)); + /* Typography */ --font-sans: "Inter", -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;