From 0a47912c4c936d052888870caef59e88c1021e24 Mon Sep 17 00:00:00 2001 From: Matias Benary Date: Thu, 11 Jul 2024 10:26:07 -0300 Subject: [PATCH 01/20] feat: wip add seacth basic --- src/components/sidebar-navigation/Search.tsx | 276 +++++++++++++++++- src/components/sidebar-navigation/Sidebar.tsx | 3 +- src/components/sidebar-navigation/styles.ts | 112 ++++++- src/hooks/useDebounce.ts | 19 ++ 4 files changed, 392 insertions(+), 18 deletions(-) create mode 100644 src/hooks/useDebounce.ts diff --git a/src/components/sidebar-navigation/Search.tsx b/src/components/sidebar-navigation/Search.tsx index a7cb0ea93..2c3f27da1 100644 --- a/src/components/sidebar-navigation/Search.tsx +++ b/src/components/sidebar-navigation/Search.tsx @@ -1,18 +1,274 @@ -import { useBosComponents } from '@/hooks/useBosComponents'; - -import { VmComponent } from '../vm/VmComponent'; +import useDebounce from '@/hooks/useDebounce'; import * as S from './styles'; +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; + +import styled from 'styled-components'; + +const Card = styled.div` + background: white; + border-radius: 10px; + padding: 16px; + display: flex; + flex-direction: column; + align-items: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + max-width: 400px; + margin-bottom: 8px; +`; + +const Tile = styled.div` + display: flex; + width: 100%; +`; +const ImageContainer = styled.div` + width: 48px; + height: 48px; + border-radius: 50%; + overflow: hidden; + margin-right: 16px; +`; + +const Image = styled.img` + width: 100%; + height: 100%; + object-fit: cover; +`; + +const Name = styled.h2` + margin: 0 0 8px; + font-size: 18px; + font-weight: bold; + &:hover { + cursor: pointer; + } +`; + +const Tagline = styled.p` + margin: 8px 0 0 8px; + font-size: 14px; + color: #666; +`; + +const TagContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 8px; +`; + +const Tag = styled.span` + background: #f0f0f0; + padding: 4px 8px; + border-radius: 16px; + font-size: 12px; + color: #333; + &:hover { + cursor: pointer; + } +`; + +const SEARCH_API_KEY_APPS = 'fc7644a5da5306311e8e9418c24fddc4'; +const APPLICATION_ID_APPS = 'B6PI9UKKJT'; +const INDEX_APPS = 'replica_prod_near-social-feed'; +const API_URL_APPS = `https://${APPLICATION_ID_APPS}-dsn.algolia.net/1/indexes/${INDEX_APPS}/query?`; + +const SEARCH_API_KEY_DOCS = '6b114c851c9921e654b5a1ffa8cffb93'; +const APPLICATION_ID_DOCS = '0LUM67N2P2'; +const INDEX_DOCS = 'near'; +const API_URL_DOCS = `https://${APPLICATION_ID_DOCS}-dsn.algolia.net/1/indexes/${INDEX_DOCS}/query?`; + +const URLS = { + Docs: { + SEARCH_API_KEY: SEARCH_API_KEY_DOCS, + APPLICATION_ID: APPLICATION_ID_DOCS, + INDEX: INDEX_DOCS, + API_URL: API_URL_DOCS, + }, + Apps: { + SEARCH_API_KEY: SEARCH_API_KEY_APPS, + APPLICATION_ID: APPLICATION_ID_APPS, + INDEX: INDEX_APPS, + API_URL: API_URL_APPS, + }, + Components: { + SEARCH_API_KEY: SEARCH_API_KEY_APPS, + APPLICATION_ID: APPLICATION_ID_APPS, + INDEX: INDEX_APPS, + API_URL: API_URL_APPS, + }, +}; export const Search = () => { - const components = useBosComponents(); + const [searchTerm, setSearchTerm] = useState(''); + const [results, setResults] = useState([]); + const [showResults, setShowResults] = useState(false); + const [activeTab, setActiveTab] = useState('docs'); + const [docs, setDocs] = useState([]); + const [apps, setApps] = useState([]); + const [components, setComponents] = useState([]); + const debouncedSearchTerm = useDebounce(searchTerm, 250); + const router = useRouter(); + + const redirect = (url: string) => () => router.push(url); + const tabs = { + docs: { + label: 'Docs', + component: docs, + }, + apps: { + label: 'Apps', + component: apps, + }, + components: { + label: 'Components', + component: components, + }, + }; + + useEffect(() => { + if (debouncedSearchTerm) { + fetchResults(); + } else { + setResults([]); + setShowResults(false); + } + }, [debouncedSearchTerm]); + + const convertUrl = (url) => url.replace(/^https:\/\/docs\.near\.org\/(.+)$/, '/documentation/$1'); + + const docsComponents = (rawResp) => { + return rawResp.hits.map((item) => { + return ( + // + + {item.hierarchy.lvl0} + {item.hierarchy.lvl1} + {item.content ? item.content.substr(0, 80) : ''} + + // + ); + }); + }; + + const appsComponents = (rawResp) => { + return Object.values(rawResp).map((item) => { + return ( + // + + + + {item.profile.name} + +
+ {item.profile.name} + + + {Object.entries(item.profile.tags).map(([key, value]) => ( + + {value} + + ))} + +
+
+ + {item.profile.tagline} + +
+ // + ); + }); + }; + + const fetchResults = async () => { + const docs = await fetchSearchHits('Docs', debouncedSearchTerm); + setDocs(docsComponents(docs)); + console.log('Search', docs); + const apps = await fetchCatalog(debouncedSearchTerm); + // console.log("fetchResults Apps", apps.values()); + setApps(appsComponents(apps)); + + const mockResults = [ + `${activeTab} Resultado 1 para "${searchTerm}"`, + `${activeTab} Resultado 2 para "${searchTerm}"`, + `${activeTab} Resultado 3 para "${searchTerm}"`, + ]; + + // setApps(mockResults); + setComponents(mockResults); + setShowResults(true); + }; + + const handleSearch = (event) => { + setSearchTerm(event.target.value); + }; + + const handleTabChange = (tabId) => { + setActiveTab(tabId); + }; + + const handleClear = () => { + setSearchTerm(''); + setShowResults(false); + }; + + // const fetchSearchHits = (facet, query, { pageNumber, configs, optionalFilters }) => { + // let body = { + // query, + // page: pageNumber ?? 0, + // optionalFilters: optionalFilters ?? ["categories:nearcatalog", "categories:widget"], + // clickAnalytics: true, + // ...configs, + // }; + + const fetchSearchHits = async (facet, query) => { + const body = { + query, + page: 0, + clickAnalytics: true, + }; + + const response = await fetch(URLS[facet].API_URL, { + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'X-Algolia-Api-Key': URLS[facet].SEARCH_API_KEY, + 'X-Algolia-Application-Id': URLS[facet].APPLICATION_ID, + }, + method: 'POST', + }); + + return await response.json(); + }; + + const fetchCatalog = async (query) => { + const response = await fetch(`https://nearcatalog.xyz/wp-json/nearcatalog/v1/search?kw=${query}`); + + return await response.json(); + }; + return ( - + 🔍 + + {searchTerm && ×} + + + + handleTabChange('docs')}> + Docs + + handleTabChange('apps')}> + Apps + + handleTabChange('components')}> + Components + + + + {tabs[activeTab].component} + ); }; diff --git a/src/components/sidebar-navigation/Sidebar.tsx b/src/components/sidebar-navigation/Sidebar.tsx index f91809484..1ce4aa1a8 100644 --- a/src/components/sidebar-navigation/Sidebar.tsx +++ b/src/components/sidebar-navigation/Sidebar.tsx @@ -48,7 +48,7 @@ export const Sidebar = () => { - + { @@ -57,7 +57,6 @@ export const Sidebar = () => { } }} > - diff --git a/src/components/sidebar-navigation/styles.ts b/src/components/sidebar-navigation/styles.ts index efb94deac..793f96e40 100644 --- a/src/components/sidebar-navigation/styles.ts +++ b/src/components/sidebar-navigation/styles.ts @@ -13,7 +13,7 @@ const fadeIn = keyframes` `; const overflowContain = css` - overflow: auto; + // overflow: auto; scroll-behavior: smooth; overscroll-behavior: contain; @@ -942,16 +942,116 @@ export const SearchSection = styled(Section)<{ `; export const SearchWrapper = styled.div` + display: flex; + align-items: center; + background-color: #f0f0f0; + border-radius: 20px; + padding: 5px 10px; + width: 224px; + position: relative; +`; + +export const SearchIcon = styled.span` + margin-right: 10px; +`; + +export const SearchInput = styled.input` + border: none; + background: transparent; + flex-grow: 1; + font-size: 16px; + outline: none; +`; + +export const ClearButton = styled.button` + background: none; + border: none; + cursor: pointer; + font-size: 18px; + color: #999; +`; + +export const TabContainer = styled.div` + display: flex; + margin-bottom: 10px; width: 100%; - height: 40px; +`; - > div, - > div > label, - > div > label > div { - height: 100%; +export const Tab = styled.button<{ $active?: boolean }>` + padding: 10px; + border: none; + background-color: ${(props) => (props.$active ? '#007bff' : '#f0f0f0')}; + color: ${(props) => (props.$active ? 'white' : 'black')}; + cursor: pointer; + flex: 1; + + &:hover { + background-color: ${(props) => (props.$active ? '#0056b3' : '#e0e0e0')}; } `; +export const ResultsPopup = styled.div<{ $show?: boolean }>` + position: absolute; + top: 100%; + left: 0; + width: 400px; + max-height: 300px; + overflow-y: auto; + background-color: white; + border: 1px solid #ccc; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + ${(props) => + props.$show + ? css` + display: block; + ` + : css` + display: none; + `} +`; + +export const ResultItem = styled.div` + padding: 10px; +`; + +export const CardDocs = styled.a` + width: 100%; + display: block; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + max-width: 400px; + text-decoration: none; + text-align: left; + margin-bottom: 8px; + + &:hover { + cursor: pointer; + text-decoration: none; + background-color: #f0f0f0; + } +`; + +export const TitleDocs = styled.h2` + font-size: 18px; + font-weight: 500; + margin: 0; + font-weight: bold; +`; + +export const SubtitleDocs = styled.h3` + font-size: 14px; + font-weight: normal; + margin: 4px 0 12px; +`; + +export const ContentDocs = styled.p` + font-size: 14px; + color: #333; + margin: 0; + line-height: 1.4; +`; + export const SearchIconWrapper = styled.div<{ $expanded: boolean; }>` diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 000000000..3825381f6 --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,19 @@ +import { useState, useEffect } from 'react'; + +function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} + +export default useDebounce; From 555e5bb57ae721ded7d7331fc9f6f81ca269d721 Mon Sep 17 00:00:00 2001 From: Matias Benary Date: Fri, 12 Jul 2024 14:58:12 -0300 Subject: [PATCH 02/20] feat: wip add search --- src/components/sidebar-navigation/Search.tsx | 221 +++++------------- .../sidebar-navigation/Search/AppsResults.tsx | 110 +++++++++ .../Search/ComponentsResults.tsx | 102 ++++++++ .../sidebar-navigation/Search/DocsResults.tsx | 89 +++++++ src/components/sidebar-navigation/styles.ts | 110 ++++----- 5 files changed, 419 insertions(+), 213 deletions(-) create mode 100644 src/components/sidebar-navigation/Search/AppsResults.tsx create mode 100644 src/components/sidebar-navigation/Search/ComponentsResults.tsx create mode 100644 src/components/sidebar-navigation/Search/DocsResults.tsx diff --git a/src/components/sidebar-navigation/Search.tsx b/src/components/sidebar-navigation/Search.tsx index 2c3f27da1..77c6fe93c 100644 --- a/src/components/sidebar-navigation/Search.tsx +++ b/src/components/sidebar-navigation/Search.tsx @@ -1,72 +1,10 @@ import useDebounce from '@/hooks/useDebounce'; import * as S from './styles'; -import { useEffect, useState } from 'react'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; +import { useEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; - -const Card = styled.div` - background: white; - border-radius: 10px; - padding: 16px; - display: flex; - flex-direction: column; - align-items: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - max-width: 400px; - margin-bottom: 8px; -`; - -const Tile = styled.div` - display: flex; - width: 100%; -`; -const ImageContainer = styled.div` - width: 48px; - height: 48px; - border-radius: 50%; - overflow: hidden; - margin-right: 16px; -`; - -const Image = styled.img` - width: 100%; - height: 100%; - object-fit: cover; -`; - -const Name = styled.h2` - margin: 0 0 8px; - font-size: 18px; - font-weight: bold; - &:hover { - cursor: pointer; - } -`; - -const Tagline = styled.p` - margin: 8px 0 0 8px; - font-size: 14px; - color: #666; -`; - -const TagContainer = styled.div` - display: flex; - flex-wrap: wrap; - gap: 8px; -`; - -const Tag = styled.span` - background: #f0f0f0; - padding: 4px 8px; - border-radius: 16px; - font-size: 12px; - color: #333; - &:hover { - cursor: pointer; - } -`; +import { AppsResults } from './Search/AppsResults'; +import { ComponentsResults } from './Search/ComponentsResults'; +import { DocsResults } from './Search/DocsResults'; const SEARCH_API_KEY_APPS = 'fc7644a5da5306311e8e9418c24fddc4'; const APPLICATION_ID_APPS = 'B6PI9UKKJT'; @@ -100,104 +38,62 @@ const URLS = { }; export const Search = () => { - const [searchTerm, setSearchTerm] = useState(''); - const [results, setResults] = useState([]); - const [showResults, setShowResults] = useState(false); - const [activeTab, setActiveTab] = useState('docs'); + const [searchTerm, setSearchTerm] = useState(''); + const [isFocus, setIsFocus] = useState(false); + const [activeTab, setActiveTab] = useState('docs'); const [docs, setDocs] = useState([]); const [apps, setApps] = useState([]); const [components, setComponents] = useState([]); const debouncedSearchTerm = useDebounce(searchTerm, 250); - const router = useRouter(); - - const redirect = (url: string) => () => router.push(url); - const tabs = { - docs: { - label: 'Docs', - component: docs, - }, - apps: { - label: 'Apps', - component: apps, - }, - components: { - label: 'Components', - component: components, - }, - }; + const componentRef = useRef(null); useEffect(() => { if (debouncedSearchTerm) { fetchResults(); - } else { - setResults([]); - setShowResults(false); } }, [debouncedSearchTerm]); - const convertUrl = (url) => url.replace(/^https:\/\/docs\.near\.org\/(.+)$/, '/documentation/$1'); + useEffect(() => { + const handleClickOutside = (event) => { + if (componentRef.current && !componentRef.current.contains(event.target)) { + setIsFocus(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); const docsComponents = (rawResp) => { - return rawResp.hits.map((item) => { - return ( - // - - {item.hierarchy.lvl0} - {item.hierarchy.lvl1} - {item.content ? item.content.substr(0, 80) : ''} - - // - ); + return rawResp.hits.map((item, index) => { + return ; }); }; const appsComponents = (rawResp) => { - return Object.values(rawResp).map((item) => { - return ( - // - - - - {item.profile.name} - -
- {item.profile.name} + return Object.values(rawResp).map((item, index) => { + return ; + }); + }; - - {Object.entries(item.profile.tags).map(([key, value]) => ( - - {value} - - ))} - -
-
- - {item.profile.tagline} - -
- // - ); + const componentComponents = (rawResp) => { + return rawResp.hits.map((item, index) => { + return ; }); }; const fetchResults = async () => { - const docs = await fetchSearchHits('Docs', debouncedSearchTerm); + const [docs, apps, components] = await Promise.all([ + fetchSearchHits('Docs', debouncedSearchTerm), + fetchCatalog(debouncedSearchTerm), + fetchSearchHits('Components', debouncedSearchTerm), + ]); + setDocs(docsComponents(docs)); - console.log('Search', docs); - const apps = await fetchCatalog(debouncedSearchTerm); - // console.log("fetchResults Apps", apps.values()); setApps(appsComponents(apps)); - - const mockResults = [ - `${activeTab} Resultado 1 para "${searchTerm}"`, - `${activeTab} Resultado 2 para "${searchTerm}"`, - `${activeTab} Resultado 3 para "${searchTerm}"`, - ]; - - // setApps(mockResults); - setComponents(mockResults); - setShowResults(true); + setComponents(componentComponents(components)); }; const handleSearch = (event) => { @@ -208,24 +104,23 @@ export const Search = () => { setActiveTab(tabId); }; + const handleOnClick = () => { + setIsFocus(true); + }; + const handleClear = () => { setSearchTerm(''); - setShowResults(false); + setDocs([]); + setApps([]); + setComponents([]); + setIsFocus(false); }; - // const fetchSearchHits = (facet, query, { pageNumber, configs, optionalFilters }) => { - // let body = { - // query, - // page: pageNumber ?? 0, - // optionalFilters: optionalFilters ?? ["categories:nearcatalog", "categories:widget"], - // clickAnalytics: true, - // ...configs, - // }; - const fetchSearchHits = async (facet, query) => { const body = { query, page: 0, + optionalFilters: ['categories:nearcatalog', 'categories:widget'], clickAnalytics: true, }; @@ -249,25 +144,33 @@ export const Search = () => { }; return ( - - 🔍 - - {searchTerm && ×} - - + + handleOnClick()} $isFocus={isFocus}> + + + {searchTerm && } + + + - handleTabChange('docs')}> + handleTabChange('docs')} $isFirst={true}> Docs handleTabChange('apps')}> Apps - handleTabChange('components')}> + handleTabChange('components')} $isLast={true}> Components - {tabs[activeTab].component} + + {activeTab === 'docs' && docs} + {activeTab === 'apps' && apps} + {activeTab === 'components' && components} + {!(docs.length || apps.length || components.length) && !searchTerm && 'Type in to search'} + {!(docs.length || apps.length || components.length) && searchTerm && 'Searching...'} + ); diff --git a/src/components/sidebar-navigation/Search/AppsResults.tsx b/src/components/sidebar-navigation/Search/AppsResults.tsx new file mode 100644 index 000000000..35e1b9b8a --- /dev/null +++ b/src/components/sidebar-navigation/Search/AppsResults.tsx @@ -0,0 +1,110 @@ +import { useRouter } from 'next/router'; +import styled from 'styled-components'; + +const Card = styled.div` + background: white; + border-radius: 10px; + padding: 16px; + display: flex; + flex-direction: column; + align-items: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + width: 100%; + margin-bottom: 8px; +`; + +const Tile = styled.div` + display: flex; + width: 100%; +`; +const ImageContainer = styled.div` + width: 48px; + height: 48px; + border-radius: 50%; + overflow: hidden; + margin-right: 16px; +`; + +const Image = styled.img` + width: 100%; + height: 100%; + object-fit: cover; +`; + +const Name = styled.h2` + margin: 0 0 8px; + font-size: 18px; + font-weight: bold; + &:hover { + cursor: pointer; + } +`; + +const Tagline = styled.p` + margin: 8px 0 0 8px; + font-size: 14px; + color: #666; +`; + +const TagContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 8px; +`; + +const Tag = styled.span` + background: #f0f0f0; + padding: 4px 8px; + border-radius: 16px; + font-size: 12px; + color: #333; + &:hover { + cursor: pointer; + } +`; + +interface Item { + slug: string; + profile: { + name: string; + tagline: string; + image: { + url: string; + }; + tags: { + [key: string]: string; + }; + }; +} + +interface AppsResultsProps { + item: Item; +} + +export const AppsResults = ({ item }) => { + const router = useRouter(); + const redirect = (url: string) => () => router.push(url); + return ( + + + + {item.profile.name} + +
+ {item.profile.name} + + + {Object.entries(item.profile.tags).map(([key, value]) => ( + + {value} + + ))} + +
+
+ + {item.profile.tagline} + +
+ ); +}; diff --git a/src/components/sidebar-navigation/Search/ComponentsResults.tsx b/src/components/sidebar-navigation/Search/ComponentsResults.tsx new file mode 100644 index 000000000..bb3067f2c --- /dev/null +++ b/src/components/sidebar-navigation/Search/ComponentsResults.tsx @@ -0,0 +1,102 @@ +import { useRouter } from 'next/navigation'; +import styled from 'styled-components'; + +const ListItem = styled.div` + background: white; + border-radius: 10px; + padding: 16px; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + width: 100%; + margin-bottom: 8px; +`; + +const Logo = styled.img` + width: 40px; + height: 40px; + border-radius: 8px; + margin-right: 15px; + cursor: pointer; +`; + +const WidgetName = styled.div` + font-weight: bold; + width: 120px; + text-overflow: ellipsis; + cursor: pointer; +`; + +const Author = styled.div` + color: #888; + font-size: 0.9em; + width: 120px; + text-overflow: ellipsis; + overflow: hidden; + cursor: pointer; +`; + +const Icon = styled.i` + cursor: pointer; +`; +interface Item { + author: string; + widget_name: string; + code: string; + receipt_hash: string; + receipt_date: number; + receipt_block_height: number; + indexed_date: number; + categories: string[]; + image: { + ipfs_cid: string; + }; + content: string; + name: string; + profile_name: string; + tags: string[]; + objectID: string; + _snippetResult: { + content: { + value: string; + matchLevel: string; + }; + }; + _highlightResult: { + author: HighlightResult; + widget_name: HighlightResult; + content: HighlightResult; + name: HighlightResult; + tags: HighlightResult[]; + }; +} + +interface HighlightResult { + value: string; + matchLevel: string; + fullyHighlighted?: boolean; + matchedWords: string[]; +} + +interface ComponentsResultsProps { + item: Item; +} + +export const ComponentsResults = ({ item }) => { + const router = useRouter(); + const redirect = (url: string) => () => router.push(url); + return ( + + + + {item.name || item.profile_name} + + @{item.author} + + + ); +}; diff --git a/src/components/sidebar-navigation/Search/DocsResults.tsx b/src/components/sidebar-navigation/Search/DocsResults.tsx new file mode 100644 index 000000000..a689bf83e --- /dev/null +++ b/src/components/sidebar-navigation/Search/DocsResults.tsx @@ -0,0 +1,89 @@ +import { useRouter } from 'next/router'; +import styled from 'styled-components'; + +export const CardDocs = styled.a` + width: 100%; + display: block; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + text-decoration: none; + text-align: left; + margin-bottom: 8px; + + &:hover { + cursor: pointer; + text-decoration: none; + background-color: #f0f0f0; + } +`; + +export const TitleDocs = styled.h2` + font-size: 18px; + font-weight: 500; + margin: 0; + font-weight: bold; +`; + +export const SubtitleDocs = styled.h3` + font-size: 14px; + font-weight: normal; + margin: 4px 0 12px; +`; + +export const ContentDocs = styled.p` + font-size: 14px; + color: #333; + margin: 0; + line-height: 1.4; +`; + +interface Item { + url: string; + url_without_anchor: string; + anchor: string; + content: null; + type: string; + hierarchy: { + lvl0: string; + lvl1: string; + }; + objectID: string; + _highlightResult: { + hierarchy: { + lvl0: HighlightResultItem; + lvl1: HighlightResultItem; + }; + hierarchy_camel: [ + { + lvl0: HighlightResultItem; + lvl1: HighlightResultItem; + }, + ]; + }; +} + +interface HighlightResultItem { + value: string; + matchLevel: 'none' | 'full' | 'partial'; + fullyHighlighted?: boolean; + matchedWords: string[]; +} + +interface DocsResultsProps { + item: Item; +} + +export const DocsResults = ({ item }) => { + const router = useRouter(); + const redirect = (url: string) => () => router.push(url); + const convertUrl = (url: string) => url.replace(/^https:\/\/docs\.near\.org\/(.+)$/, '/documentation/$1'); + + return ( + + {item.hierarchy.lvl0} + {item.hierarchy.lvl1} + {item.content ? item.content.substr(0, 80) : ''} + + ); +}; diff --git a/src/components/sidebar-navigation/styles.ts b/src/components/sidebar-navigation/styles.ts index 793f96e40..eb407ceef 100644 --- a/src/components/sidebar-navigation/styles.ts +++ b/src/components/sidebar-navigation/styles.ts @@ -944,40 +944,66 @@ export const SearchSection = styled(Section)<{ export const SearchWrapper = styled.div` display: flex; align-items: center; - background-color: #f0f0f0; border-radius: 20px; padding: 5px 10px; - width: 224px; position: relative; `; -export const SearchIcon = styled.span` - margin-right: 10px; +export const SearchContainer = styled.div<{ $isFocus?: boolean }>` + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #e3e3e0; + border-radius: 25px; + padding: 5px 10px; + width: 250px; + + transition: all 0.3s ease-in-out; + ${(props) => + props.$isFocus && + css` + box-shadow: 0 0 5px 2px rgba(136, 0, 255, 0.5); + border-color: #cfccf5; + `} `; export const SearchInput = styled.input` border: none; - background: transparent; - flex-grow: 1; - font-size: 16px; outline: none; + width: 100%; + margin-left: 10px; + font-size: 16px; + + &::placeholder { + color: #868682; + } `; -export const ClearButton = styled.button` - background: none; - border: none; +export const IconContainer = styled.div` + display: flex; + align-items: center; cursor: pointer; - font-size: 18px; - color: #999; +`; + +export const SearchIcon = styled.div<{ $isFocus?: boolean }>` + color: #868682; + transition: color 0.3s ease-in-out; + + ${(props) => + props.$isFocus && + css` + color: #6f42c1; + `} `; export const TabContainer = styled.div` display: flex; margin-bottom: 10px; width: 100%; + border-radius: 16px; `; -export const Tab = styled.button<{ $active?: boolean }>` +export const Tab = styled.button<{ $active?: boolean; $isFirst?: boolean; $isLast?: boolean }>` padding: 10px; border: none; background-color: ${(props) => (props.$active ? '#007bff' : '#f0f0f0')}; @@ -985,6 +1011,18 @@ export const Tab = styled.button<{ $active?: boolean }>` cursor: pointer; flex: 1; + ${(props) => + props.$isFirst && + css` + border-top-left-radius: 16px; + `} + + ${(props) => + props.$isLast && + css` + border-top-right-radius: 16px; + `} + &:hover { background-color: ${(props) => (props.$active ? '#0056b3' : '#e0e0e0')}; } @@ -994,9 +1032,9 @@ export const ResultsPopup = styled.div<{ $show?: boolean }>` position: absolute; top: 100%; left: 0; - width: 400px; - max-height: 300px; - overflow-y: auto; + width: 550px; + border-radius: 16px 16px 0 0; + /* max-height: 300px; */ background-color: white; border: 1px solid #ccc; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); @@ -1012,44 +1050,8 @@ export const ResultsPopup = styled.div<{ $show?: boolean }>` export const ResultItem = styled.div` padding: 10px; -`; - -export const CardDocs = styled.a` - width: 100%; - display: block; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 16px; - max-width: 400px; - text-decoration: none; - text-align: left; - margin-bottom: 8px; - - &:hover { - cursor: pointer; - text-decoration: none; - background-color: #f0f0f0; - } -`; - -export const TitleDocs = styled.h2` - font-size: 18px; - font-weight: 500; - margin: 0; - font-weight: bold; -`; - -export const SubtitleDocs = styled.h3` - font-size: 14px; - font-weight: normal; - margin: 4px 0 12px; -`; - -export const ContentDocs = styled.p` - font-size: 14px; - color: #333; - margin: 0; - line-height: 1.4; + max-height: 300px; + overflow-y: scroll; `; export const SearchIconWrapper = styled.div<{ From f0b8baf1f2ba1e661d646d0b8bb42381108f7e83 Mon Sep 17 00:00:00 2001 From: Matias Benary Date: Thu, 18 Jul 2024 11:55:52 -0300 Subject: [PATCH 03/20] feat: added search in navbar and page --- src/components/Pagination.tsx | 93 +++++++++ src/components/sidebar-navigation/Search.tsx | 151 +++++--------- .../sidebar-navigation/Search/AppsResults.tsx | 3 +- .../Search/ComponentsResults.tsx | 3 +- .../sidebar-navigation/Search/DocsResults.tsx | 5 +- src/components/sidebar-navigation/styles.ts | 19 +- src/pages/search.tsx | 190 ++++++++++++++++++ src/utils/angoliaSearchApi.ts | 67 ++++++ src/utils/catalogSearchApi.ts | 4 + 9 files changed, 430 insertions(+), 105 deletions(-) create mode 100644 src/components/Pagination.tsx create mode 100644 src/pages/search.tsx create mode 100644 src/utils/angoliaSearchApi.ts create mode 100644 src/utils/catalogSearchApi.ts diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx new file mode 100644 index 000000000..afc9e4d48 --- /dev/null +++ b/src/components/Pagination.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import styled from 'styled-components'; + +interface PageButtonProps { + active?: boolean; + disabled?: boolean; + onClick?: () => void; +} + +interface PaginationProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; +} + +const PaginationContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +`; + +const PageButton = styled.button` + padding: 4px 12px; + border-radius: 9999px; + background-color: ${(props) => (props.active ? '#3b82f6' : 'transparent')}; + color: ${(props) => (props.active ? 'white' : 'inherit')}; + cursor: ${(props) => (props.disabled ? 'default' : 'pointer')}; + opacity: ${(props) => (props.disabled ? 0.5 : 1)}; + transition: background-color 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + height: 36px; + border: 1px solid #eee; + + &:hover:not(:disabled) { + background-color: ${(props) => (props.active ? '#3b82f6' : '#e5e7eb')}; + } +`; + +const Pagination: React.FC = ({ currentPage, totalPages, onPageChange }) => { + const pageNumbers: (number | string)[] = []; + const maxVisiblePages = 3; + + if (totalPages <= maxVisiblePages) { + for (let i = 1; i <= totalPages; i++) { + pageNumbers.push(i); + } + } else { + const leftOffset = Math.max(currentPage - Math.floor(maxVisiblePages / 2), 1); + const rightOffset = Math.min(leftOffset + maxVisiblePages - 1, totalPages); + + if (leftOffset > 2) { + pageNumbers.push(1, '...'); + } else if (leftOffset === 2) { + pageNumbers.push(1); + } + + for (let i = leftOffset; i <= rightOffset; i++) { + pageNumbers.push(i); + } + + if (rightOffset < totalPages - 1) { + pageNumbers.push('...', totalPages); + } else if (rightOffset === totalPages - 1) { + pageNumbers.push(totalPages); + } + } + + return ( + + onPageChange(currentPage - 1)} disabled={currentPage === 1}> + + + {pageNumbers.map((number, index) => ( + typeof number === 'number' && onPageChange(number)} + active={number === currentPage} + disabled={typeof number !== 'number'} + > + {number} + + ))} + onPageChange(currentPage + 1)} disabled={currentPage === totalPages}> + + + + ); +}; + +export default Pagination; diff --git a/src/components/sidebar-navigation/Search.tsx b/src/components/sidebar-navigation/Search.tsx index 77c6fe93c..0ae9b4cf3 100644 --- a/src/components/sidebar-navigation/Search.tsx +++ b/src/components/sidebar-navigation/Search.tsx @@ -5,51 +5,38 @@ import { useEffect, useRef, useState } from 'react'; import { AppsResults } from './Search/AppsResults'; import { ComponentsResults } from './Search/ComponentsResults'; import { DocsResults } from './Search/DocsResults'; +import Link from 'next/link'; +import { fetchSearchHits } from '@/utils/angoliaSearchApi'; +import { fetchCatalog } from '@/utils/catalogSearchApi'; -const SEARCH_API_KEY_APPS = 'fc7644a5da5306311e8e9418c24fddc4'; -const APPLICATION_ID_APPS = 'B6PI9UKKJT'; -const INDEX_APPS = 'replica_prod_near-social-feed'; -const API_URL_APPS = `https://${APPLICATION_ID_APPS}-dsn.algolia.net/1/indexes/${INDEX_APPS}/query?`; - -const SEARCH_API_KEY_DOCS = '6b114c851c9921e654b5a1ffa8cffb93'; -const APPLICATION_ID_DOCS = '0LUM67N2P2'; -const INDEX_DOCS = 'near'; -const API_URL_DOCS = `https://${APPLICATION_ID_DOCS}-dsn.algolia.net/1/indexes/${INDEX_DOCS}/query?`; - -const URLS = { - Docs: { - SEARCH_API_KEY: SEARCH_API_KEY_DOCS, - APPLICATION_ID: APPLICATION_ID_DOCS, - INDEX: INDEX_DOCS, - API_URL: API_URL_DOCS, - }, - Apps: { - SEARCH_API_KEY: SEARCH_API_KEY_APPS, - APPLICATION_ID: APPLICATION_ID_APPS, - INDEX: INDEX_APPS, - API_URL: API_URL_APPS, - }, - Components: { - SEARCH_API_KEY: SEARCH_API_KEY_APPS, - APPLICATION_ID: APPLICATION_ID_APPS, - INDEX: INDEX_APPS, - API_URL: API_URL_APPS, - }, -}; +const TABS = ['Docs', 'Apps', 'Components'] as const; +type TabType = (typeof TABS)[number]; export const Search = () => { const [searchTerm, setSearchTerm] = useState(''); - const [isFocus, setIsFocus] = useState(false); - const [activeTab, setActiveTab] = useState('docs'); - const [docs, setDocs] = useState([]); - const [apps, setApps] = useState([]); - const [components, setComponents] = useState([]); const debouncedSearchTerm = useDebounce(searchTerm, 250); const componentRef = useRef(null); + const [isFocus, setIsFocus] = useState(false); + + const [activeTab, setActiveTab] = useState('Docs'); + const [results, setResults] = useState>({ + Docs: null, + Apps: null, + Components: null, + }); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (debouncedSearchTerm) { + fetchResults(); + } + }, [debouncedSearchTerm]); useEffect(() => { if (debouncedSearchTerm) { fetchResults(); + } else { + setResults({ Docs: null, Apps: null, Components: null }); } }, [debouncedSearchTerm]); @@ -66,41 +53,44 @@ export const Search = () => { }; }, []); - const docsComponents = (rawResp) => { - return rawResp.hits.map((item, index) => { - return ; - }); - }; - - const appsComponents = (rawResp) => { - return Object.values(rawResp).map((item, index) => { - return ; - }); - }; - - const componentComponents = (rawResp) => { - return rawResp.hits.map((item, index) => { - return ; - }); - }; - const fetchResults = async () => { + setIsLoading(true); + const [docs, apps, components] = await Promise.all([ fetchSearchHits('Docs', debouncedSearchTerm), fetchCatalog(debouncedSearchTerm), fetchSearchHits('Components', debouncedSearchTerm), ]); - setDocs(docsComponents(docs)); - setApps(appsComponents(apps)); - setComponents(componentComponents(components)); + setResults({ + Docs: renderResults('Docs', docs), + Apps: renderResults('Apps', apps), + Components: renderResults('Components', components), + }); + + setIsLoading(false); + }; + + const renderResults = (type: TabType, rawResp: any) => { + if (!rawResp || (Array.isArray(rawResp.hits) && !rawResp.hits.length)) { + return
No results found for "{debouncedSearchTerm}"
; + } + + switch (type) { + case 'Docs': + return rawResp.hits.map((item: any, index: number) => ); + case 'Apps': + return Object.values(rawResp).map((item: any, index: number) => ); + case 'Components': + return rawResp.hits.map((item: any, index: number) => ); + } }; const handleSearch = (event) => { setSearchTerm(event.target.value); }; - const handleTabChange = (tabId) => { + const handleTabChange = (tabId: TabType) => { setActiveTab(tabId); }; @@ -110,37 +100,7 @@ export const Search = () => { const handleClear = () => { setSearchTerm(''); - setDocs([]); - setApps([]); - setComponents([]); - setIsFocus(false); - }; - - const fetchSearchHits = async (facet, query) => { - const body = { - query, - page: 0, - optionalFilters: ['categories:nearcatalog', 'categories:widget'], - clickAnalytics: true, - }; - - const response = await fetch(URLS[facet].API_URL, { - body: JSON.stringify(body), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - 'X-Algolia-Api-Key': URLS[facet].SEARCH_API_KEY, - 'X-Algolia-Application-Id': URLS[facet].APPLICATION_ID, - }, - method: 'POST', - }); - - return await response.json(); - }; - - const fetchCatalog = async (query) => { - const response = await fetch(`https://nearcatalog.xyz/wp-json/nearcatalog/v1/search?kw=${query}`); - - return await response.json(); + setResults({ Docs: null, Apps: null, Components: null }); }; return ( @@ -153,24 +113,23 @@ export const Search = () => { - handleTabChange('docs')} $isFirst={true}> + handleTabChange('Docs')} $isFirst={true}> Docs - handleTabChange('apps')}> + handleTabChange('Apps')}> Apps - handleTabChange('components')} $isLast={true}> + handleTabChange('Components')} $isLast={true}> Components - {activeTab === 'docs' && docs} - {activeTab === 'apps' && apps} - {activeTab === 'components' && components} - {!(docs.length || apps.length || components.length) && !searchTerm && 'Type in to search'} - {!(docs.length || apps.length || components.length) && searchTerm && 'Searching...'} + {isLoading ? 'Searching...' : searchTerm ? results[activeTab] : 'Type in to search'} + + See all results +
); diff --git a/src/components/sidebar-navigation/Search/AppsResults.tsx b/src/components/sidebar-navigation/Search/AppsResults.tsx index 35e1b9b8a..2bf67d890 100644 --- a/src/components/sidebar-navigation/Search/AppsResults.tsx +++ b/src/components/sidebar-navigation/Search/AppsResults.tsx @@ -11,6 +11,7 @@ const Card = styled.div` box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); width: 100%; margin-bottom: 8px; + max-width: 500px; `; const Tile = styled.div` @@ -81,7 +82,7 @@ interface AppsResultsProps { item: Item; } -export const AppsResults = ({ item }) => { +export const AppsResults: React.FC = ({ item }) => { const router = useRouter(); const redirect = (url: string) => () => router.push(url); return ( diff --git a/src/components/sidebar-navigation/Search/ComponentsResults.tsx b/src/components/sidebar-navigation/Search/ComponentsResults.tsx index bb3067f2c..836fd6d1d 100644 --- a/src/components/sidebar-navigation/Search/ComponentsResults.tsx +++ b/src/components/sidebar-navigation/Search/ComponentsResults.tsx @@ -11,6 +11,7 @@ const ListItem = styled.div` box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); width: 100%; margin-bottom: 8px; + max-width: 500px; `; const Logo = styled.img` @@ -83,7 +84,7 @@ interface ComponentsResultsProps { item: Item; } -export const ComponentsResults = ({ item }) => { +export const ComponentsResults: React.FC = ({ item }) => { const router = useRouter(); const redirect = (url: string) => () => router.push(url); return ( diff --git a/src/components/sidebar-navigation/Search/DocsResults.tsx b/src/components/sidebar-navigation/Search/DocsResults.tsx index a689bf83e..b7f6b50ac 100644 --- a/src/components/sidebar-navigation/Search/DocsResults.tsx +++ b/src/components/sidebar-navigation/Search/DocsResults.tsx @@ -10,6 +10,7 @@ export const CardDocs = styled.a` text-decoration: none; text-align: left; margin-bottom: 8px; + max-width: 500px; &:hover { cursor: pointer; @@ -42,7 +43,7 @@ interface Item { url: string; url_without_anchor: string; anchor: string; - content: null; + content: null | string; type: string; hierarchy: { lvl0: string; @@ -74,7 +75,7 @@ interface DocsResultsProps { item: Item; } -export const DocsResults = ({ item }) => { +export const DocsResults: React.FC = ({ item }) => { const router = useRouter(); const redirect = (url: string) => () => router.push(url); const convertUrl = (url: string) => url.replace(/^https:\/\/docs\.near\.org\/(.+)$/, '/documentation/$1'); diff --git a/src/components/sidebar-navigation/styles.ts b/src/components/sidebar-navigation/styles.ts index eb407ceef..e4ee7e238 100644 --- a/src/components/sidebar-navigation/styles.ts +++ b/src/components/sidebar-navigation/styles.ts @@ -1006,10 +1006,10 @@ export const TabContainer = styled.div` export const Tab = styled.button<{ $active?: boolean; $isFirst?: boolean; $isLast?: boolean }>` padding: 10px; border: none; - background-color: ${(props) => (props.$active ? '#007bff' : '#f0f0f0')}; - color: ${(props) => (props.$active ? 'white' : 'black')}; + background-color: ${(props) => (props.$active ? '#e0e0e0' : '#f0f0f0')}; cursor: pointer; flex: 1; + border-bottom: 2px solid ${(props) => (props.$active ? '#007bff' : 'transparent')}; ${(props) => props.$isFirst && @@ -1024,7 +1024,8 @@ export const Tab = styled.button<{ $active?: boolean; $isFirst?: boolean; $isLas `} &:hover { - background-color: ${(props) => (props.$active ? '#0056b3' : '#e0e0e0')}; + background-color: #e0e0e0; + border-bottom: 2px solid #007bff; } `; @@ -1033,7 +1034,7 @@ export const ResultsPopup = styled.div<{ $show?: boolean }>` top: 100%; left: 0; width: 550px; - border-radius: 16px 16px 0 0; + border-radius: 16px 16px 16px 16px; /* max-height: 300px; */ background-color: white; border: 1px solid #ccc; @@ -1050,10 +1051,18 @@ export const ResultsPopup = styled.div<{ $show?: boolean }>` export const ResultItem = styled.div` padding: 10px; - max-height: 300px; + height: 300px; overflow-y: scroll; `; +export const Footer = styled.div` + text-align: right; + width: 100%; + padding: 16px; + border-radius: 0 0 16px 16px; + background-color: #f0f0f0; +`; + export const SearchIconWrapper = styled.div<{ $expanded: boolean; }>` diff --git a/src/pages/search.tsx b/src/pages/search.tsx new file mode 100644 index 000000000..9e983ae1c --- /dev/null +++ b/src/pages/search.tsx @@ -0,0 +1,190 @@ +import Pagination from '@/components/Pagination'; +import { AppsResults } from '@/components/sidebar-navigation/Search/AppsResults'; +import { ComponentsResults } from '@/components/sidebar-navigation/Search/ComponentsResults'; +import { DocsResults } from '@/components/sidebar-navigation/Search/DocsResults'; +import useDebounce from '@/hooks/useDebounce'; +import { useDefaultLayout } from '@/hooks/useLayout'; +import { fetchSearchHits } from '@/utils/angoliaSearchApi'; +import { fetchCatalog } from '@/utils/catalogSearchApi'; +import type { NextPageWithLayout } from '@/utils/types'; +import { useSearchParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; + +const SearchContainer = styled.div` + font-family: Arial, sans-serif; + max-width: 800px; + margin: 0 auto; + padding: 20px; +`; + +const Title = styled.h1` + font-size: 36px; + font-weight: bold; + text-align: center; + margin-bottom: 10px; +`; + +const Subtitle = styled.p` + font-size: 18px; + color: #666; + text-align: center; + margin-bottom: 30px; +`; + +const SearchBox = styled.div` + display: flex; + align-items: center; + border: 1px solid #ccc; + border-radius: 25px; + padding: 10px 15px; + margin-bottom: 20px; +`; + +const SearchInput = styled.input` + flex-grow: 1; + border: none; + font-size: 16px; + outline: none; + margin-left: 10px; +`; + +const TabContainer = styled.div` + display: flex; + justify-content: center; + margin-bottom: 30px; +`; + +const Tab = styled.button` + background: ${(props) => (props.active ? '#eee' : 'transparent')}; + border: none; + border-radius: 20px; + padding: 8px 16px; + margin: 0 5px; + cursor: pointer; + font-size: 14px; + + &:hover { + background: #eee; + } +`; + +const TABS = ['Docs', 'Apps', 'Components'] as const; +type TabType = (typeof TABS)[number]; + +const SearchPage: NextPageWithLayout = () => { + const searchParams = useSearchParams(); + const [searchTerm, setSearchTerm] = useState(searchParams.get('query') || ''); + const [activeTab, setActiveTab] = useState('Docs'); + const [results, setResults] = useState>({ + Docs: null, + Apps: null, + Components: null, + }); + const [isLoading, setIsLoading] = useState(false); + const debouncedSearchTerm = useDebounce(searchTerm, 250); + + useEffect(() => { + if (debouncedSearchTerm) { + fetchResults(); + } else { + setResults({ Docs: null, Apps: null, Components: null }); + } + }, [debouncedSearchTerm]); + + const fetchResults = async () => { + setIsLoading(true); + + const [docs, apps, components] = await Promise.all([ + fetchSearchHits('Docs', debouncedSearchTerm), + fetchCatalog(debouncedSearchTerm), + fetchSearchHits('Components', debouncedSearchTerm), + ]); + + setResults({ + Docs: renderResults('Docs', docs), + Apps: renderResults('Apps', apps), + Components: renderResults('Components', components), + }); + + setIsLoading(false); + }; + + const renderResults = (type: TabType, rawResp: any) => { + if (!rawResp || (Array.isArray(rawResp.hits) && !rawResp.hits.length)) { + return No results found for "{debouncedSearchTerm}"; + } + + switch (type) { + case 'Docs': + return ( + <> + {rawResp.hits.map((item: any, index: number) => ( + + ))} + handlePageChange(type, page)} + /> + + ); + case 'Apps': + return Object.values(rawResp).map((item: any, index: number) => ); + case 'Components': + return ( + <> + {rawResp.hits.map((item: any, index: number) => ( + + ))} + handlePageChange(type, page)} + /> + + ); + } + }; + + const handlePageChange = async (type: TabType, page: number) => { + const newResults = await fetchSearchHits(type, debouncedSearchTerm, page - 1); + setResults((prev) => ({ ...prev, [type]: renderResults(type, newResults) })); + }; + + const handleClear = () => { + setSearchTerm(''); + setResults({ Docs: null, Apps: null, Components: null }); + }; + + return ( + + Search + Explore and find everything on the Blockchain Operating System + + +
+ setSearchTerm(e.target.value)} + /> + {searchTerm && } + + + + {TABS.map((tab) => ( + setActiveTab(tab)}> + {tab} + + ))} + + + {isLoading ? 'Searching...' : searchTerm ? results[activeTab] : 'Type in to search'} + + ); +}; +SearchPage.getLayout = useDefaultLayout; + +export default SearchPage; diff --git a/src/utils/angoliaSearchApi.ts b/src/utils/angoliaSearchApi.ts new file mode 100644 index 000000000..768fcebec --- /dev/null +++ b/src/utils/angoliaSearchApi.ts @@ -0,0 +1,67 @@ +const SEARCH_API_KEY_APPS = 'fc7644a5da5306311e8e9418c24fddc4'; +const APPLICATION_ID_APPS = 'B6PI9UKKJT'; +const INDEX_APPS = 'replica_prod_near-social-feed'; +const API_URL_APPS = `https://${APPLICATION_ID_APPS}-dsn.algolia.net/1/indexes/${INDEX_APPS}/query?`; + +const SEARCH_API_KEY_DOCS = '6b114c851c9921e654b5a1ffa8cffb93'; +const APPLICATION_ID_DOCS = '0LUM67N2P2'; +const INDEX_DOCS = 'near'; +const API_URL_DOCS = `https://${APPLICATION_ID_DOCS}-dsn.algolia.net/1/indexes/${INDEX_DOCS}/query?`; + +const URLS_INFO = { + Docs: { + SEARCH_API_KEY: SEARCH_API_KEY_DOCS, + APPLICATION_ID: APPLICATION_ID_DOCS, + INDEX: INDEX_DOCS, + API_URL: API_URL_DOCS, + }, + Apps: { + SEARCH_API_KEY: SEARCH_API_KEY_APPS, + APPLICATION_ID: APPLICATION_ID_APPS, + INDEX: INDEX_APPS, + API_URL: API_URL_APPS, + }, + Components: { + SEARCH_API_KEY: SEARCH_API_KEY_APPS, + APPLICATION_ID: APPLICATION_ID_APPS, + INDEX: INDEX_APPS, + API_URL: API_URL_APPS, + }, +}; + +type Facet = 'Docs' | 'Apps' | 'Components'; + +interface URLS { + [key: string]: { + API_URL: string; + SEARCH_API_KEY: string; + APPLICATION_ID: string; + }; +} + +declare const URLS: URLS; + +export const fetchSearchHits = async (facet: Facet, query: string, page = 0) => { + const body = { + query, + page, + optionalFilters: ['categories:nearcatalog', 'categories:widget'], + clickAnalytics: true, + }; + + const response = await fetch(URLS_INFO[facet].API_URL, { + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'X-Algolia-Api-Key': URLS_INFO[facet].SEARCH_API_KEY, + 'X-Algolia-Application-Id': URLS_INFO[facet].APPLICATION_ID, + }, + method: 'POST', + }); + + if (!response.ok) { + throw new Error(`Failed to fetch search hits: ${response.statusText}`); + } + + return await response.json(); +}; diff --git a/src/utils/catalogSearchApi.ts b/src/utils/catalogSearchApi.ts new file mode 100644 index 000000000..8f0cfe073 --- /dev/null +++ b/src/utils/catalogSearchApi.ts @@ -0,0 +1,4 @@ +export const fetchCatalog = async (query: string) => { + const response = await fetch(`https://nearcatalog.xyz/wp-json/nearcatalog/v1/search?kw=${query}`); + return await response.json(); +}; From 149fda93b7ecfe059da69bca27f8b82c7e716153 Mon Sep 17 00:00:00 2001 From: Matias Benary Date: Thu, 18 Jul 2024 15:29:37 -0300 Subject: [PATCH 04/20] fix: fix CI/Lint --- src/components/sidebar-navigation/Search.tsx | 8 ++++---- src/pages/search.tsx | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/sidebar-navigation/Search.tsx b/src/components/sidebar-navigation/Search.tsx index 0ae9b4cf3..24b94f9bd 100644 --- a/src/components/sidebar-navigation/Search.tsx +++ b/src/components/sidebar-navigation/Search.tsx @@ -15,7 +15,7 @@ type TabType = (typeof TABS)[number]; export const Search = () => { const [searchTerm, setSearchTerm] = useState(''); const debouncedSearchTerm = useDebounce(searchTerm, 250); - const componentRef = useRef(null); + const componentRef = useRef(null); const [isFocus, setIsFocus] = useState(false); const [activeTab, setActiveTab] = useState('Docs'); @@ -41,8 +41,8 @@ export const Search = () => { }, [debouncedSearchTerm]); useEffect(() => { - const handleClickOutside = (event) => { - if (componentRef.current && !componentRef.current.contains(event.target)) { + const handleClickOutside = (event: MouseEvent) => { + if (componentRef.current && !componentRef.current.contains(event.target as Node)) { setIsFocus(false); } }; @@ -86,7 +86,7 @@ export const Search = () => { } }; - const handleSearch = (event) => { + const handleSearch = (event: React.ChangeEvent) => { setSearchTerm(event.target.value); }; diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 9e983ae1c..5e6be787d 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -55,7 +55,9 @@ const TabContainer = styled.div` margin-bottom: 30px; `; -const Tab = styled.button` +const Tab = styled.button<{ + active: boolean; +}>` background: ${(props) => (props.active ? '#eee' : 'transparent')}; border: none; border-radius: 20px; From 3957b064324b780e561ffe060d16537ba5f3a63f Mon Sep 17 00:00:00 2001 From: Matias Benary Date: Mon, 22 Jul 2024 11:36:28 -0300 Subject: [PATCH 05/20] feat: improve visual details --- src/components/sidebar-navigation/Search.tsx | 2 +- .../sidebar-navigation/Search/AppsResults.tsx | 6 ++--- .../Search/ComponentsResults.tsx | 4 +-- .../sidebar-navigation/Search/DocsResults.tsx | 5 +--- src/components/sidebar-navigation/styles.ts | 27 +++++-------------- src/pages/search.tsx | 2 +- ...ngoliaSearchApi.ts => algoliaSearchApi.ts} | 0 7 files changed, 13 insertions(+), 33 deletions(-) rename src/utils/{angoliaSearchApi.ts => algoliaSearchApi.ts} (100%) diff --git a/src/components/sidebar-navigation/Search.tsx b/src/components/sidebar-navigation/Search.tsx index 24b94f9bd..f8a0b845d 100644 --- a/src/components/sidebar-navigation/Search.tsx +++ b/src/components/sidebar-navigation/Search.tsx @@ -6,7 +6,7 @@ import { AppsResults } from './Search/AppsResults'; import { ComponentsResults } from './Search/ComponentsResults'; import { DocsResults } from './Search/DocsResults'; import Link from 'next/link'; -import { fetchSearchHits } from '@/utils/angoliaSearchApi'; +import { fetchSearchHits } from '@/utils/algoliaSearchApi'; import { fetchCatalog } from '@/utils/catalogSearchApi'; const TABS = ['Docs', 'Apps', 'Components'] as const; diff --git a/src/components/sidebar-navigation/Search/AppsResults.tsx b/src/components/sidebar-navigation/Search/AppsResults.tsx index 2bf67d890..7150810dd 100644 --- a/src/components/sidebar-navigation/Search/AppsResults.tsx +++ b/src/components/sidebar-navigation/Search/AppsResults.tsx @@ -3,15 +3,13 @@ import styled from 'styled-components'; const Card = styled.div` background: white; - border-radius: 10px; - padding: 16px; + padding: 16px 0; display: flex; flex-direction: column; align-items: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); width: 100%; - margin-bottom: 8px; max-width: 500px; + margin-bottom: 8px; `; const Tile = styled.div` diff --git a/src/components/sidebar-navigation/Search/ComponentsResults.tsx b/src/components/sidebar-navigation/Search/ComponentsResults.tsx index 836fd6d1d..a16647430 100644 --- a/src/components/sidebar-navigation/Search/ComponentsResults.tsx +++ b/src/components/sidebar-navigation/Search/ComponentsResults.tsx @@ -4,13 +4,11 @@ import styled from 'styled-components'; const ListItem = styled.div` background: white; border-radius: 10px; - padding: 16px; + padding: 16px 0; display: flex; justify-content: space-between; align-items: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); width: 100%; - margin-bottom: 8px; max-width: 500px; `; diff --git a/src/components/sidebar-navigation/Search/DocsResults.tsx b/src/components/sidebar-navigation/Search/DocsResults.tsx index b7f6b50ac..d16ac020e 100644 --- a/src/components/sidebar-navigation/Search/DocsResults.tsx +++ b/src/components/sidebar-navigation/Search/DocsResults.tsx @@ -5,17 +5,14 @@ export const CardDocs = styled.a` width: 100%; display: block; border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 16px; + padding: 16px 0; text-decoration: none; text-align: left; - margin-bottom: 8px; max-width: 500px; &:hover { cursor: pointer; text-decoration: none; - background-color: #f0f0f0; } `; diff --git a/src/components/sidebar-navigation/styles.ts b/src/components/sidebar-navigation/styles.ts index e4ee7e238..6374661ab 100644 --- a/src/components/sidebar-navigation/styles.ts +++ b/src/components/sidebar-navigation/styles.ts @@ -944,7 +944,7 @@ export const SearchSection = styled(Section)<{ export const SearchWrapper = styled.div` display: flex; align-items: center; - border-radius: 20px; + border-radius: 10px; padding: 5px 10px; position: relative; `; @@ -1000,31 +1000,18 @@ export const TabContainer = styled.div` display: flex; margin-bottom: 10px; width: 100%; - border-radius: 16px; + padding: 12px 0; `; export const Tab = styled.button<{ $active?: boolean; $isFirst?: boolean; $isLast?: boolean }>` padding: 10px; border: none; - background-color: ${(props) => (props.$active ? '#e0e0e0' : '#f0f0f0')}; cursor: pointer; flex: 1; border-bottom: 2px solid ${(props) => (props.$active ? '#007bff' : 'transparent')}; - - ${(props) => - props.$isFirst && - css` - border-top-left-radius: 16px; - `} - - ${(props) => - props.$isLast && - css` - border-top-right-radius: 16px; - `} - + background-color: white; + font-size: 12px; &:hover { - background-color: #e0e0e0; border-bottom: 2px solid #007bff; } `; @@ -1034,10 +1021,11 @@ export const ResultsPopup = styled.div<{ $show?: boolean }>` top: 100%; left: 0; width: 550px; - border-radius: 16px 16px 16px 16px; + border-radius: 10px; /* max-height: 300px; */ background-color: white; border: 1px solid #ccc; + padding: 0 24px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); ${(props) => props.$show @@ -1059,8 +1047,7 @@ export const Footer = styled.div` text-align: right; width: 100%; padding: 16px; - border-radius: 0 0 16px 16px; - background-color: #f0f0f0; + border-radius: 0 0 10px 10px; `; export const SearchIconWrapper = styled.div<{ diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 5e6be787d..20a97a1c7 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -4,7 +4,7 @@ import { ComponentsResults } from '@/components/sidebar-navigation/Search/Compon import { DocsResults } from '@/components/sidebar-navigation/Search/DocsResults'; import useDebounce from '@/hooks/useDebounce'; import { useDefaultLayout } from '@/hooks/useLayout'; -import { fetchSearchHits } from '@/utils/angoliaSearchApi'; +import { fetchSearchHits } from '@/utils/algoliaSearchApi'; import { fetchCatalog } from '@/utils/catalogSearchApi'; import type { NextPageWithLayout } from '@/utils/types'; import { useSearchParams } from 'next/navigation'; diff --git a/src/utils/angoliaSearchApi.ts b/src/utils/algoliaSearchApi.ts similarity index 100% rename from src/utils/angoliaSearchApi.ts rename to src/utils/algoliaSearchApi.ts From 837ee36dd2a183d088f064cab47ece337e9c994a Mon Sep 17 00:00:00 2001 From: Matias Benary Date: Wed, 24 Jul 2024 09:46:48 -0300 Subject: [PATCH 06/20] fix: details --- src/components/sidebar-navigation/Search.tsx | 20 +++++++++++++++---- .../Search/ComponentsResults.tsx | 12 ++++++++++- src/components/sidebar-navigation/Sidebar.tsx | 11 +++++++++- .../sidebar-navigation/SmallScreenHeader.tsx | 5 ++++- src/components/sidebar-navigation/styles.ts | 6 +++--- src/utils/algoliaSearchApi.ts | 13 +++++++++++- 6 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/components/sidebar-navigation/Search.tsx b/src/components/sidebar-navigation/Search.tsx index f8a0b845d..15be8c3bb 100644 --- a/src/components/sidebar-navigation/Search.tsx +++ b/src/components/sidebar-navigation/Search.tsx @@ -1,6 +1,6 @@ import useDebounce from '@/hooks/useDebounce'; import * as S from './styles'; -import { useEffect, useRef, useState } from 'react'; +import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { AppsResults } from './Search/AppsResults'; import { ComponentsResults } from './Search/ComponentsResults'; @@ -12,7 +12,19 @@ import { fetchCatalog } from '@/utils/catalogSearchApi'; const TABS = ['Docs', 'Apps', 'Components'] as const; type TabType = (typeof TABS)[number]; -export const Search = () => { +export const Search = forwardRef((props, ref) => { + const inputRef = useRef(null); + + useImperativeHandle(ref, () => ({ + focus: () => { + if (inputRef.current) { + console.log('hola don pepito'); + + inputRef.current.focus(); + } + }, + })); + const [searchTerm, setSearchTerm] = useState(''); const debouncedSearchTerm = useDebounce(searchTerm, 250); const componentRef = useRef(null); @@ -107,7 +119,7 @@ export const Search = () => { handleOnClick()} $isFocus={isFocus}> - + {searchTerm && } @@ -133,4 +145,4 @@ export const Search = () => { ); -}; +}); diff --git a/src/components/sidebar-navigation/Search/ComponentsResults.tsx b/src/components/sidebar-navigation/Search/ComponentsResults.tsx index a16647430..c2764afe5 100644 --- a/src/components/sidebar-navigation/Search/ComponentsResults.tsx +++ b/src/components/sidebar-navigation/Search/ComponentsResults.tsx @@ -1,4 +1,5 @@ import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import styled from 'styled-components'; const ListItem = styled.div` @@ -85,11 +86,20 @@ interface ComponentsResultsProps { export const ComponentsResults: React.FC = ({ item }) => { const router = useRouter(); const redirect = (url: string) => () => router.push(url); + const defaultImage = 'bafkreifc4burlk35hxom3klq4mysmslfirj7slueenbj7ddwg7pc6ixomu'; + const [imageSrc, setImageSrc] = useState(item?.image?.ipfs_cid || defaultImage); + + const handleImageError = () => { + setImageSrc(defaultImage); + }; + return ( {item.name || item.profile_name} diff --git a/src/components/sidebar-navigation/Sidebar.tsx b/src/components/sidebar-navigation/Sidebar.tsx index 1ce4aa1a8..43942aeeb 100644 --- a/src/components/sidebar-navigation/Sidebar.tsx +++ b/src/components/sidebar-navigation/Sidebar.tsx @@ -11,6 +11,7 @@ import { Search } from './Search'; import { useNavigationStore } from './store'; import * as S from './styles'; import { currentPathMatchesRoute } from './utils'; +import { useRef } from 'react'; export const Sidebar = () => { const router = useRouter(); @@ -22,6 +23,13 @@ export const Sidebar = () => { const tooltipsDisabled = isSidebarExpanded; const signedIn = useAuthStore((store) => store.signedIn); const { requestAuthentication } = useSignInRedirect(); + const inputRef = useRef(null); + + const handleFocus = () => { + if (inputRef.current) { + inputRef.current.focus(); + } + }; const handleCreateAccount = () => { requestAuthentication(true); @@ -48,15 +56,16 @@ export const Sidebar = () => { - { if (!isSidebarExpanded) { toggleExpandedSidebar(); + handleFocus(); } }} > + diff --git a/src/components/sidebar-navigation/SmallScreenHeader.tsx b/src/components/sidebar-navigation/SmallScreenHeader.tsx index 7947dbc2c..8a05c2b3d 100644 --- a/src/components/sidebar-navigation/SmallScreenHeader.tsx +++ b/src/components/sidebar-navigation/SmallScreenHeader.tsx @@ -9,8 +9,11 @@ import NearIconSvg from './icons/near-icon.svg'; import { LargeScreenProfileDropdown } from './LargeScreenProfileDropdown'; import { useNavigationStore } from './store'; import * as S from './styles'; +import { useRouter } from 'next/router'; export const SmallScreenHeader = () => { + const router = useRouter(); + const redirect = (url: string) => () => router.push(url); const components = useBosComponents(); const isOpenedOnSmallScreens = useNavigationStore((store) => store.isOpenedOnSmallScreens); const toggleExpandedSidebarOnSmallScreens = useNavigationStore((store) => store.toggleExpandedSidebarOnSmallScreens); @@ -50,7 +53,7 @@ export const SmallScreenHeader = () => { {signedIn ? ( -