From c4c699cc446b5f36a4ed3829a3e6ae8bae9397bd Mon Sep 17 00:00:00 2001 From: giorgia Date: Thu, 21 Jan 2021 12:14:41 +0000 Subject: [PATCH] Merged in prefetch-api (pull request #60) Prefetch api * getInitialProps and redirect - half way * ssr * make load more works * finish search with SSR, add isLoading --- .prettierrc.json | 2 +- components/AllResultsView/AllResultsView.tsx | 24 +- components/ImagesView/ImagesView.tsx | 14 +- components/NewsView/NewsView.tsx | 12 +- components/SearchBar/SearchBar.tsx | 9 +- components/TabsMenu/TabsMenu.tsx | 3 +- components/VideosView/VideosView.tsx | 13 +- helpers/_cookies.tsx | 6 +- package-lock.json | 5 + package.json | 4 +- pages_/search.tsx | 504 ++++++++++--------- 11 files changed, 297 insertions(+), 299 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index 5ad0627..8bfa5a7 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,7 +1,7 @@ { "singleQuote": true, "jsxSingleQuote": true, - "printWidth": 300, + "printWidth": 120, "semi": false, "endOfLine": "auto" } \ No newline at end of file diff --git a/components/AllResultsView/AllResultsView.tsx b/components/AllResultsView/AllResultsView.tsx index db1075f..fa53651 100644 --- a/components/AllResultsView/AllResultsView.tsx +++ b/components/AllResultsView/AllResultsView.tsx @@ -41,11 +41,10 @@ type organicItemsObj = { pixelUrl: string } interface Prop { - organicResults: { items: organicItemsObj[] } - sponsoredResults: { items: sponsoredItemsObj[] } - relatedSearches: { items: relatedLinks[] } - imageResults: { items: image[] } - videoResults: { items: any[] } + organicItems: organicItemsObj[] + sponsoredItems: sponsoredItemsObj[] + relatedSearches: relatedLinks[] + imagesItems: image[] batches?: { [x: number]: any[] } } @@ -70,10 +69,7 @@ const AllResultsView = ({ results, searchQuery }: ResultsProp) => { if (!results) return <> - const { organicResults, sponsoredResults, relatedSearches, imageResults, batches } = results - const organicItems = organicResults.items - const sponsoredItems = sponsoredResults.items - const images = imageResults.items + const { organicItems, sponsoredItems, relatedSearches, imagesItems, batches } = results const mainlineSponsor = [] const sidebarSponsor = [] @@ -83,7 +79,7 @@ const AllResultsView = ({ results, searchQuery }: ResultsProp) => { } const firstBatchOrganic = !organicItems.length ? [] : organicItems.slice(0, 3) const secondBatchOrganic = !organicItems.length ? [] : organicItems.slice(3, organicItems.length) - const combinedResults = [...mainlineSponsor, ...firstBatchOrganic, images, ...secondBatchOrganic] + const combinedResults = [...mainlineSponsor, ...firstBatchOrganic, imagesItems, ...secondBatchOrganic] return ( <> @@ -91,9 +87,9 @@ const AllResultsView = ({ results, searchQuery }: ResultsProp) => {

{t('search:no_result_found_query', { query: searchQuery })}

) : (
- {relatedSearches.items.length !== 0 && ( + {relatedSearches.length !== 0 && (
- +
)}
@@ -108,9 +104,9 @@ const AllResultsView = ({ results, searchQuery }: ResultsProp) => { } })} - {relatedSearches.items.length !== 0 && ( + {relatedSearches.length !== 0 && (
- +
)} diff --git a/components/ImagesView/ImagesView.tsx b/components/ImagesView/ImagesView.tsx index 8b966c0..3b89df6 100644 --- a/components/ImagesView/ImagesView.tsx +++ b/components/ImagesView/ImagesView.tsx @@ -8,22 +8,16 @@ type ImagesProp = { imageUrl: string thumbnailUrl: string pixelUrl: string + length: () => number + map: any } - -interface Prop { - imageResults: { items: ImagesProp[] } -} - interface ResultsProp { - results: Prop + images: ImagesProp query: string } -const ImagesView = ({ results, query }: ResultsProp) => { +const ImagesView = ({ images, query }: ResultsProp) => { const { t } = useTranslation() - if (!results) return <> - - const images = results.imageResults.items return ( <> diff --git a/components/NewsView/NewsView.tsx b/components/NewsView/NewsView.tsx index 793a3d8..5adf9b7 100644 --- a/components/NewsView/NewsView.tsx +++ b/components/NewsView/NewsView.tsx @@ -11,20 +11,16 @@ type NewsObj = { provider: string description: string pixelUrl: string + length: () => number + map: any } - -interface Prop { - newsResults: { items: NewsObj[] } -} - interface ResultsProp { - results: Prop + news: NewsObj query: string } -const NewsView = ({ results, query }: ResultsProp) => { +const NewsView = ({ news, query }: ResultsProp) => { const { t } = useTranslation() - const news = results.newsResults?.items return ( <> diff --git a/components/SearchBar/SearchBar.tsx b/components/SearchBar/SearchBar.tsx index 89323fc..2ff35b7 100644 --- a/components/SearchBar/SearchBar.tsx +++ b/components/SearchBar/SearchBar.tsx @@ -38,7 +38,7 @@ const SearchBar = ({ big }: SearchProps) => { openInNewTab: userState.openInNewTab, }, }) - const { register } = methods + const { handleSubmit, register } = methods if (type !== typeValue && typeValue !== initType) { setTypeValue(type) @@ -113,6 +113,11 @@ const SearchBar = ({ big }: SearchProps) => { } }, [searchValue]) + function onSubmit({ q }) { + setSearchValue(q) + search() + } + function resetDropdown(event) { setIsSuggestionOpen(false) setHighlightIndex(null) @@ -148,7 +153,7 @@ const SearchBar = ({ big }: SearchProps) => { return (
-
+
number + map: any } interface ResultsProp { - results: Prop + videos: VideosProp query: string } -const VideosView = ({ results, query }: ResultsProp) => { +const VideosView = ({ videos, query }: ResultsProp) => { const { t } = useTranslation() - const videos = results.videoResults.items - + console.log({ videos }) return ( <> {!videos.length ? ( diff --git a/helpers/_cookies.tsx b/helpers/_cookies.tsx index cf66fff..42b3d22 100644 --- a/helpers/_cookies.tsx +++ b/helpers/_cookies.tsx @@ -4,6 +4,10 @@ export const COOKIE_NAME_NEW_TAB = 'efw_new_tab' export const COOKIE_NAME_COOKIE_CONSENT = 'efw_cookie_consent_accepted' export const COOKIE_NAME_SEARCH_COUNT = 'efw_search_count' +export const splitCookies = function (cookies, cookieName) { + return cookies.split(`; ${cookieName}=`).pop().split(';').shift() +} + export function set(cookieName, cookieValue, expiryDays = 365) { const dateNow = new Date() dateNow.setTime(dateNow.getTime() + expiryDays * 240 * 600 * 600 * 1000) @@ -12,5 +16,5 @@ export function set(cookieName, cookieValue, expiryDays = 365) { } export const get = function get(cookieName) { - return `; ${document.cookie}`.split(`; ${cookieName}=`).pop().split(';').shift() + return splitCookies(`; ${document.cookie}`, cookieName) } diff --git a/package-lock.json b/package-lock.json index 827a448..b56a49c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21317,6 +21317,11 @@ "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", "dev": true }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", diff --git a/package.json b/package.json index 61658ae..796c46f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "repository": "https://bitbucket.org/elliotforwater/elliotforwater_v4/src/master/", "main": "index.js", "scripts": { - "dev": "next-translate && next dev", + "dev": "next-translate && NODE_TLS_REJECT_UNAUTHORIZED='0' next dev", "build": "next-translate && next build", "start": "next-translate && next start", "export": "next build && next export", @@ -108,4 +108,4 @@ "stylelint" ] } -} +} \ No newline at end of file diff --git a/pages_/search.tsx b/pages_/search.tsx index 650c541..a117950 100644 --- a/pages_/search.tsx +++ b/pages_/search.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState, useContext, ReactElement } from 'react' -import { useRouter } from 'next/router' +import React, { useEffect, useState, useContext } from 'react' +import Router, { useRouter } from 'next/router' import { UserContext } from '../context/UserContext' import useTranslation from 'next-translate/useTranslation' import dynamic from 'next/dynamic' @@ -9,6 +9,7 @@ import TabsMenu from '../components/TabsMenu/TabsMenu' import Loader from '../components/Loader/Loader' import LoadMore from '../components/LoadMore/LoadMore' import { formatNumber, queryNoWitheSpace } from '../helpers/_utils' +import { splitCookies, get, COOKIE_NAME_ADULT_FILTER } from '../helpers/_cookies' const AllResultsView = dynamic(() => import('../components/AllResultsView/AllResultsView'), { loading: () => , @@ -28,48 +29,13 @@ interface tabProp { id: number resultType: string title: string - content: ReactElement -} - -const initStateTab = { - id: null, - resultType: '', - title: '', - content: null, -} - -type itemsProp = { - items: any[] - numResults?: number -} - -type batchesProp = { - [x: number]: any[] -} -interface resultsProp { - organicResults: null | itemsProp - sponsoredResults: null | itemsProp - relatedSearches: null | itemsProp - imageResults: null | itemsProp - videoResults: null | itemsProp - newsResults: null | itemsProp - batches?: batchesProp -} - -interface ContainerProps { - isLoading: boolean - component: ReactElement - resultsBatch: number - incrementResultsBatch: (nextIndex: any) => void - showLoadMore: boolean - numResults?: number } const MAX_RESULTS = { - web: { name: 'organicResults', maxPerReq: 10 }, - image: { name: 'imageResults', maxPerReq: 150 }, - video: { name: 'videoResults', maxPerReq: 50 }, - news: { name: 'newsResults', maxPerReq: 100 }, + web: 10, + image: 150, + video: 50, + news: 100, } const TAB_MENU = [ @@ -77,292 +43,210 @@ const TAB_MENU = [ id: 1, resultType: 'web', title: 'search:all', - content: null, }, { id: 2, resultType: 'image', title: 'search:images', - content: null, }, { id: 3, resultType: 'video', title: 'search:videos', - content: null, }, { id: 4, resultType: 'news', title: 'search:news', - content: null, }, { id: 5, resultType: 'map', title: 'search:map', - content: null, }, ] -function Container({ isLoading, component, resultsBatch, incrementResultsBatch, showLoadMore, numResults }: ContainerProps) { - const { t } = useTranslation() - - if (isLoading) { - return - } else { - return ( - <> - {component} - {showLoadMore && ( -
- - -
- )} - {numResults !== undefined && numResults > 0 && ( -
-

{t('search:tot_results', { tot_results: formatNumber(numResults) })}

-

- - {t('search:microsoft_result')} - -

- -
- )} - - ) - } +function findTabByType(type?: string): tabProp { + return TAB_MENU.find((tab) => type === tab.resultType) } -function SearchPage({ query, type }) { +function SearchPage({ + query, + type, + errorCode, + activeTab, + organicTotResults, + organicItems, + sponsoredItems, + imagesItems, + videoItems, + newsItems, + relatedSearches, +}) { const { t } = useTranslation() const router = useRouter() const { userState } = useContext(UserContext) - const [results, setResults] = useState(null) - const [activeTab, setActiveTab] = useState(initStateTab) - const [isError, setIsError] = useState<{ status: number }>({ status: 200 }) - const [isLoading, setIsLoading] = useState(false) const [resultsBatch, setResultsBatch] = useState(0) const [showLoadMore, setShowLoadMore] = useState(false) - const [tabMenu, setTabMenu] = useState(TAB_MENU) - const queryNoWithe = queryNoWitheSpace(query) - - useEffect(() => { - setTabMenu((prev) => { - const newTabs = [...prev] - newTabs.map((tab) => { - switch (tab.resultType) { - case 'web': - return (tab.content = } />) - case 'image': - return (tab.content = } />) - case 'video': - return (tab.content = } />) - case 'news': - return (tab.content = } />) - case 'map': - return (tab.content = ) - } - }) + const [isLoadingMore, setIsLoadingMore] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [content, setContent] = useState(null) + const queryNoWhite = queryNoWitheSpace(query) + const [errorStatus, setStatusCode] = useState(errorCode) - return newTabs - }) - }, [isLoading, showLoadMore]) + const [allResults, setAllResults] = useState({ organicItems, sponsoredItems, imagesItems, relatedSearches }) + const [images, setImages] = useState(imagesItems) + const [videos, setVideos] = useState(videoItems) + const [news, setNews] = useState(newsItems) useEffect(() => { - setActiveTab(findTab()) - }, [results]) + switch (type) { + case 'web': + setAllResults({ organicItems, sponsoredItems, imagesItems, relatedSearches }) + break + case 'image': + setImages(imagesItems) + break + case 'video': + setVideos(videoItems) + break + case 'news': + setNews(newsItems) + break + } + }, [query, type]) useEffect(() => { - if (type === 'map') { - return setActiveTab(findTab()) - } + let content + setIsLoading(true) + switch (type) { + case 'web': + content = + setIsLoading(false) + allResults?.organicItems.length ? setShowLoadMore(true) : setShowLoadMore(false) + break - const fetchData = async () => { - try { - setIsLoading(true) - const res = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/searchresults/${type}?query=${queryNoWithe}&` + - new URLSearchParams({ - AdultContentFilter: `${userState.adultContentFilter}`, - }) - ) + case 'image': + content = + setIsLoading(false) + images.length ? setShowLoadMore(true) : setShowLoadMore(false) + break - if (res.ok) { - const json = await res.json() - setIsError({ status: 200 }) - - setResults((prev) => { - if (prev && prev.batches) { - const newResults = { - ...json, - batches: {}, - } - - return newResults - } else { - return json - } - }) + case 'video': + content = + setIsLoading(false) + videos.length ? setShowLoadMore(true) : setShowLoadMore(false) + break - setActiveTab(findTab()) - window.scrollTo(0, 0) - handleShowLoadMore(json) + case 'news': + content = + setIsLoading(false) + news.length ? setShowLoadMore(true) : setShowLoadMore(false) + break - setIsLoading(false) - } else { - setIsError({ status: 400 }) - setIsLoading(false) - } - } catch (err) { - console.error('Error while fetching Search API:', err) - setIsError({ status: 500 }) + case 'map': + content = setIsLoading(false) - } + setShowLoadMore(false) + break } - fetchData() - }, [query, type]) + setContent(content) + }, [allResults, images, videos, news, type, query]) useEffect(() => { if (resultsBatch === 0 || type === 'map') return const fetchData = async () => { - // TODO: Proper Batch Loading state - load just new batch, not whole container - try { - setShowLoadMore(false) - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/searchresults/${type}?query=${queryNoWithe}&page=${resultsBatch}`) + setIsLoadingMore(true) + + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/searchresults/${type}?` + + new URLSearchParams({ + query: `${queryNoWhite}`, + page: `${resultsBatch}`, + AdultContentFilter: `${userState.adultContentFilter}`, + }) + ) if (res.ok) { const json = await res.json() - setIsError({ status: 200 }) - switch (type) { case 'web': - return setResults((prevResults) => { - const sponsoredMainline = json.sponsoredResults?.items?.filter((item) => item.placementHint === 'Mainline') + setAllResults((prevResults: any) => { + const sponsoredMainline = json.sponsoredResults?.items?.filter( + (item) => item.placementHint === 'Mainline' + ) + const newResults = { ...prevResults, batches: { [resultsBatch]: [...sponsoredMainline, ...json.organicResults?.items], }, } - handleShowLoadMore(newResults) + + const last = newResults.batches && Object.keys(newResults.batches).pop() + if (newResults.batches[last].length < MAX_RESULTS.web) { + return setShowLoadMore(false) + } + return newResults }) + setIsLoadingMore(false) + return case 'image': - return setResults((prevResults) => { - const newResults = { - ...prevResults, - } - newResults.imageResults.items = prevResults.imageResults.items.concat(json.imageResults?.items) + setImages((prevResults) => { + const newResults = [...prevResults, ...json.imageResults?.items] - handleShowLoadMore(newResults) + if (newResults.length < MAX_RESULTS.image) { + setShowLoadMore(false) + } return newResults }) + setIsLoadingMore(false) + return + case 'video': - return setResults((prevResults) => { - const newResults = { - ...prevResults, + setVideos((prevResults) => { + const newResults = [...prevResults, ...json.videoResults?.items] + + if (newResults.length < MAX_RESULTS.video) { + setShowLoadMore(false) } - newResults.videoResults.items = prevResults.videoResults.items.concat(json.videoResults?.items) - handleShowLoadMore(newResults) return newResults }) + setIsLoadingMore(false) + return + case 'news': - return setResults((prevResults) => { - const newResults = { - ...prevResults, + setNews((prevResults) => { + const newResults = [...prevResults, ...json.newsResults?.items] + + if (newResults.length < MAX_RESULTS.news) { + setShowLoadMore(false) } - newResults.newsResults.items = prevResults.newsResults.items.concat(json.newsResults?.items) - handleShowLoadMore(newResults) return newResults }) + + setIsLoadingMore(false) + return } } else { - setIsError({ status: 400 }) + setStatusCode(400) } } catch (err) { console.error('Error while fetching Search API:', err) - setIsError({ status: 500 }) + setStatusCode(500) } } fetchData() }, [resultsBatch]) - function handleShowLoadMore(newResults) { - const typeResultName = MAX_RESULTS[type].name - const maxResultsPerReq = MAX_RESULTS[type].maxPerReq - - if (type === 'web') { - // no results for 'query' - if (!newResults.organicResults.items.length) { - return setShowLoadMore(false) - } - - // first render with results - if (!newResults.batches) { - return setShowLoadMore(true) - } - - // no more results available - const last = newResults.batches && Object.keys(newResults.batches).pop() - if (newResults.batches[last].length < maxResultsPerReq) { - return setShowLoadMore(false) - } - } else if (!newResults[typeResultName]?.items.length || newResults[typeResultName]?.items.length < maxResultsPerReq) { - return setShowLoadMore(false) - } - - setShowLoadMore(true) - } - function handleSetResultBatch(nextIndex) { setResultsBatch(nextIndex) } @@ -371,18 +255,40 @@ function SearchPage({ query, type }) { router.push(`search?query=${query}&type=${nextActiveTab.resultType}`) } - function findTab(newType?: string): tabProp { - return tabMenu.find((tab) => (newType || type) === tab.resultType) - } - return (
- +
-
{isError.status !== 200 ? : activeTab.content}
+
+ {isLoading && } + {errorStatus && } + + {!errorStatus && !isLoading && content} + + {!isLoading && showLoadMore && ( +
+ {!isLoadingMore ? ( + + ) : ( + + )} +
+ )} + + {!isLoading && organicTotResults > 0 && ( +
+

{t('search:tot_results', { tot_results: formatNumber(organicTotResults) })}

+

+ + {t('search:microsoft_result')} + +

+
+ )} +
@@ -421,12 +358,77 @@ function SearchPage({ query, type }) { ) } -export async function getServerSideProps({ query }) { +type itemsProp = { + items: any[] + numResults?: number +} +interface resultsObj { + organicResults: null | itemsProp + sponsoredResults: null | itemsProp + relatedSearches: null | itemsProp + imageResults: null | itemsProp + videoResults: null | itemsProp + newsResults: null | itemsProp +} + +// cannot use getServerSideProps yet: https://github.com/vercel/next.js/discussions/17269 +SearchPage.getInitialProps = async ({ req, res, query }) => { + const searchQuery = query.query + const type = query.type + if (!searchQuery || !type) { + if (res) { + res.writeHead(301, { Location: '/' }) + return res.end() + } else { + return Router.push('/') + } + } + + const queryNoWhite = queryNoWitheSpace(searchQuery) + let results: resultsObj = null + let activeTab = findTabByType(type) + const userAgent = req ? req.headers['user-agent'] : navigator.userAgent + const cookies = req ? req.headers.cookie : get(COOKIE_NAME_ADULT_FILTER) + + if (type === 'map') { + activeTab = findTabByType('map') + } else { + try { + const data = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/searchresults/${type}?` + + new URLSearchParams({ + query: `${queryNoWhite}`, + AdultContentFilter: splitCookies(cookies, COOKIE_NAME_ADULT_FILTER), + }), + { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': userAgent, + 'Access-Control-Allow-Headers': '*', + }, + } + ) + + if (data.ok) { + results = await data.json() + } + } catch (err) { + console.error('Error while fetching Search API:', err) + } + } + return { - props: { - query: query.query, - type: query.type, - }, + query: searchQuery, + type, + errorCode: res && res.statusCode !== 200 ? res.statusCode : null, + organicTotResults: results?.organicResults?.numResults ?? null, + organicItems: results?.organicResults?.items ?? [], + sponsoredItems: results?.sponsoredResults?.items ?? [], + imagesItems: results?.imageResults?.items ?? [], + videoItems: results?.videoResults?.items ?? [], + newsItems: results?.newsResults?.items ?? [], + relatedSearches: results?.relatedSearches?.items ?? [], + activeTab, } }