Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: bookmark search #638

Merged
merged 13 commits into from
Oct 11, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions packages/shared/src/components/BookmarkFeedLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, {
ReactElement,
ReactNode,
useContext,
useMemo,
useState,
} from 'react';
import Link from 'next/link';
import MagnifyingIcon from '../../icons/magnifying.svg';
import BookmarkIcon from '../../icons/bookmark.svg';
import sizeN from '../../macros/sizeN.macro';
import { BOOKMARKS_FEED_QUERY, SEARCH_BOOKMARKS_QUERY } from '../graphql/feed';
import { headerHeight } from '../styles/sizes';
import AuthContext from '../contexts/AuthContext';
import { Button } from './buttons/Button';
import { CustomFeedHeader, FeedPage } from './utilities';
import SearchEmptyScreen from './SearchEmptyScreen';
import Feed, { FeedProps } from './Feed';

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],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the bookmarks while not logging in should not be possible.
If the user is not logged in maybe we should disable the query

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You actually can't get the bookmarks feed if not logged in.
The BookmarkFeedPage will redirect you if the token is refreshed.

I assumed this was a second fallback in the split second.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh and fully tested locally yes

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]);
rebelchris marked this conversation as resolved.
Show resolved Hide resolved

if (showEmptyScreen) {
return (
<main
className="flex fixed inset-0 flex-col justify-center items-center px-6 withNavBar text-theme-label-secondary"
style={{ marginTop: headerHeight }}
>
{children}
<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>
);
}
rebelchris marked this conversation as resolved.
Show resolved Hide resolved

return (
<FeedPage>
{children}
<CustomFeedHeader className="relative">
{!isSearchOn && (
<>
<Link href="/bookmarks/search">
<a
aria-label="Search bookmarks"
className="flex relative flex-row justify-center items-center font-bold no-underline border cursor-pointer select-none shadow-none iconOnly small btn typo-callout focus-outline btn-tertiary"
>
<MagnifyingIcon />
</a>
rebelchris marked this conversation as resolved.
Show resolved Hide resolved
</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