Skip to content

Commit

Permalink
feat: bookmark search
Browse files Browse the repository at this point in the history
Added a bookmark search and bookmark search suggestion feature.

DD-78 #done
  • Loading branch information
rebelchris authored Oct 11, 2021
1 parent 9573c36 commit 135e48e
Show file tree
Hide file tree
Showing 11 changed files with 488 additions and 101 deletions.
35 changes: 35 additions & 0 deletions packages/shared/src/components/BookmarkEmptyScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { ReactElement } from 'react';
import Link from 'next/link';
import BookmarkIcon from '../../icons/bookmark.svg';
import { headerHeight } from '../styles/sizes';
import sizeN from '../../macros/sizeN.macro';
import { Button } from './buttons/Button';

export default function BookmarkEmptyScreen(): ReactElement {
return (
<main
className="flex fixed inset-0 flex-col justify-center items-center px-6 withNavBar text-theme-label-secondary"
style={{ marginTop: headerHeight }}
>
<BookmarkIcon
className="m-0 icon text-theme-label-tertiary"
style={{ fontSize: sizeN(20) }}
/>
<h1
className="my-4 text-center text-theme-label-primary typo-title1"
style={{ maxWidth: '32.5rem' }}
>
Your bookmark list is empty.
</h1>
<p className="mb-10 text-center" style={{ maxWidth: '32.5rem' }}>
Go back to your feed and bookmark posts you’d like to keep or read
later. Each post you bookmark will be stored here.
</p>
<Link href="/" passHref>
<Button className="btn-primary" tag="a" buttonSize="large">
Back to feed
</Button>
</Link>
</main>
);
}
79 changes: 79 additions & 0 deletions packages/shared/src/components/BookmarkFeedLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, {
ReactElement,
ReactNode,
useContext,
useMemo,
useState,
} from 'react';
import Link from 'next/link';
import MagnifyingIcon from '../../icons/magnifying.svg';
import { BOOKMARKS_FEED_QUERY, SEARCH_BOOKMARKS_QUERY } from '../graphql/feed';
import AuthContext from '../contexts/AuthContext';
import { CustomFeedHeader, FeedPage } from './utilities';
import SearchEmptyScreen from './SearchEmptyScreen';
import Feed, { FeedProps } from './Feed';
import BookmarkEmptyScreen from './BookmarkEmptyScreen';
import { Button } from './buttons/Button';

export type BookmarkFeedLayoutProps = {
isSearchOn: boolean;
searchQuery?: string;
children?: ReactNode;
searchChildren: ReactNode;
onSearchButtonClick?: () => unknown;
};

export default function BookmarkFeedLayout({
searchQuery,
isSearchOn,
searchChildren,
children,
}: BookmarkFeedLayoutProps): ReactElement {
const { user, tokenRefreshed } = useContext(AuthContext);
const [showEmptyScreen, setShowEmptyScreen] = useState(false);

const feedProps = useMemo<FeedProps<unknown>>(() => {
if (isSearchOn && searchQuery) {
return {
feedQueryKey: ['bookmarks', user?.id ?? 'anonymous', searchQuery],
query: SEARCH_BOOKMARKS_QUERY,
variables: { query: searchQuery },
emptyScreen: <SearchEmptyScreen />,
className: 'my-3',
};
}
return {
feedQueryKey: ['bookmarks', user?.id ?? 'anonymous'],
query: BOOKMARKS_FEED_QUERY,
className: 'my-3',
onEmptyFeed: () => setShowEmptyScreen(true),
};
}, [isSearchOn && searchQuery, user]);

if (showEmptyScreen) {
return <BookmarkEmptyScreen />;
}

return (
<FeedPage>
{children}
<CustomFeedHeader className="relative">
{!isSearchOn && (
<>
<Link href="/bookmarks/search">
<Button
href="/bookmarks/search"
aria-label="Search bookmarks"
icon={<MagnifyingIcon />}
/>
</Link>
<div className="mx-4 w-px h-full bg-theme-bg-tertiary">&nbsp;</div>
<span className="font-bold typo-callout">Bookmarks</span>
</>
)}
{isSearchOn ? searchChildren : undefined}
</CustomFeedHeader>
{tokenRefreshed && <Feed {...feedProps} />}
</FeedPage>
);
}
19 changes: 13 additions & 6 deletions packages/shared/src/components/PostsSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SearchField } from './fields/SearchField';
import { useAutoComplete } from '../hooks/useAutoComplete';
import { apiUrl } from '../lib/config';
import { SEARCH_POST_SUGGESTIONS } from '../graphql/search';
import { SEARCH_BOOKMARKS_SUGGESTIONS } from '../graphql/feed';

const AutoCompleteMenu = dynamic(() => import('./fields/AutoCompleteMenu'), {
ssr: false,
Expand All @@ -15,12 +16,14 @@ export type PostsSearchProps = {
initialQuery?: string;
onSubmitQuery: (query: string) => Promise<unknown>;
closeSearch: () => unknown;
suggestionType?: string;
};

export default function PostsSearch({
initialQuery: initialQueryProp,
onSubmitQuery,
closeSearch,
suggestionType = 'searchPostSuggestions',
}: PostsSearchProps): ReactElement {
const searchBoxRef = useRef<HTMLDivElement>();
const [initialQuery, setInitialQuery] = useState<string>();
Expand All @@ -33,11 +36,16 @@ export default function PostsSearch({
}>(null);
const [items, setItems] = useState<string[]>([]);

const SEARCH_URL =
suggestionType === 'searchPostSuggestions'
? SEARCH_POST_SUGGESTIONS
: SEARCH_BOOKMARKS_SUGGESTIONS;

const { data: searchResults, isLoading } = useQuery<{
searchPostSuggestions: { hits: { title: string }[] };
[suggestionType: string]: { hits: { title: string }[] };
}>(
['searchPostSuggestions', query],
() => request(`${apiUrl}/graphql`, SEARCH_POST_SUGGESTIONS, { query }),
[suggestionType, query],
() => request(`${apiUrl}/graphql`, SEARCH_URL, { query }),
{
enabled: !!query,
},
Expand All @@ -62,12 +70,11 @@ export default function PostsSearch({

useEffect(() => {
if (!isLoading) {
if (!items?.length && searchResults?.searchPostSuggestions?.hits.length) {
if (!items?.length && searchResults?.[suggestionType]?.hits.length) {
showSuggestions();
}
setItems(
searchResults?.searchPostSuggestions?.hits.map((hit) => hit.title) ??
[],
searchResults?.[suggestionType]?.hits.map((hit) => hit.title) ?? [],
);
}
}, [searchResults, isLoading]);
Expand Down
24 changes: 24 additions & 0 deletions packages/shared/src/graphql/feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,30 @@ export const BOOKMARKS_FEED_QUERY = gql`
${FEED_POST_CONNECTION_FRAGMENT}
`;

export const SEARCH_BOOKMARKS_QUERY = gql`
query SearchBookmarks(
$loggedIn: Boolean! = false
$first: Int
$after: String
$query: String!
) {
page: searchBookmarks(first: $first, after: $after, query: $query) {
...FeedPostConnection
}
}
${FEED_POST_CONNECTION_FRAGMENT}
`;

export const SEARCH_BOOKMARKS_SUGGESTIONS = gql`
query SearchBookmarksSuggestions($query: String!) {
searchBookmarksSuggestions(query: $query) {
hits {
title
}
}
}
`;

export const SEARCH_POSTS_QUERY = gql`
query SearchPosts(
$loggedIn: Boolean! = false
Expand Down
15 changes: 14 additions & 1 deletion packages/webapp/__tests__/BookmarksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ const renderComponent = (
}}
>
<SettingsContext.Provider value={settingsContext}>
<BookmarksPage />
{BookmarksPage.getLayout(
<BookmarksPage />,
{},
BookmarksPage.layoutProps,
)}
</SettingsContext.Provider>
</OnboardingContext.Provider>
</AuthContext.Provider>
Expand Down Expand Up @@ -143,3 +147,12 @@ it('should show empty screen when feed is empty', async () => {
expect(elements.length).toBeFalsy();
});
});

it('should set href to the search permalink', async () => {
renderComponent();
await waitForNock();
await waitFor(async () => {
const searchBtn = await screen.findByLabelText('Search bookmarks');
expect(searchBtn).toHaveAttribute('href', '/bookmarks/search');
});
});
Loading

0 comments on commit 135e48e

Please sign in to comment.