1 - import React, { useState } from 'react'; 1 + import React, { useEffect , useRef , useState } from 'react'; 2 2 import styles from './SearchResults.module.scss'; 3 - import Header from 'components/ui/Header/Header'; 4 3 import Footer from 'components/ui/Footer/Footer'; 5 4 import Container from 'components/ui/Container/Container'; 6 5 import { Icon } from '../../ui/Icon/Icon'; 7 6 import PackagePreview from '../../ui/PackagePreview/PackagePreview'; 8 - import SearchBar from '../../ui/SearchBar/SearchBar'; 9 7 import SearchedResource from '../../ui/SearchedResource/SearchedResource'; 10 8 import { CardProps } from '../../ui/Card/Card'; 11 9 import CardGroup from '../../ui/CardGroup/CardGroup'; 12 10 import CardList from '../../ui/CardList/CardList'; 13 11 import CardGroups from 'components/ui/CardGroups/CardGroups'; 14 - import SidebarCategory from '../../ui/SidebarCategory/SidebarCategory'; 15 - import { Button } from '../../ui'; 12 + import LoadingBar, { LoadingBarRef } from 'react-top-loading-bar'; 13 + import DefaultHeader from '../../ui/ Header / DefaultHeader '; 14 + import SearchResultsSidebar from 'components/ui/SearchResultsSidebar/SearchResultsSidebar'; 15 + import { SearchedResourceSkeleton } from '../../ui/SearchedResource/SearchedResourceSkeleton'; 16 + import { PackagePreviewSkeleton } from '../../ui/PackagePreview/PackagePreviewSkeleton'; 17 + import { CardListSkeleton } from '../../ui/CardList/CardListSkeleton'; 18 + 19 + type Props = { 20 + pageLoading?: boolean; 21 + }; 22 + 23 + export default function SearchResults({ pageLoading = false }: Props) { 24 + const [loading, setLoading] = useState<boolean>(pageLoading); 25 + 26 + const loadingRef = useRef<LoadingBarRef>(null); 16 27 17 - export default function SearchResults() { 28 + // FIXME: just for demo purposes to show how loading bar works 29 + // Documentation: https://github.com/klendi/react-top-loading-bar 30 + // Starts the loading indicator with a random starting value between 20-30 (or startingValue), 31 + // then repetitively after an refreshRate (in milliseconds), increases it by a random value 32 + // between 2-10. This continues until it reaches 90% of the indicator's width. 33 + useEffect(() => { 34 + loadingRef?.current?.continuousStart(10, 5000); 35 + 36 + // After 10 seconds makes the loading indicator reach 100% of his width and then fade. 37 + setTimeout(() => { 38 + loadingRef?.current?.complete(); 39 + setLoading(false); 40 + }, 60000); 41 + }, []); 42 + 18 43 // TODO: mock data, remove later 19 44 const similarCards: CardProps[] = [ 20 45 { skipped 99 lines 120 145 }, 121 146 ]; 122 147 148 + const metaItems = [ 149 + { 150 + icon: <Icon kind='weight' width={24} height={24} />, 151 + text: '159 kb webpack bundle size', 152 + }, 153 + { 154 + icon: <Icon kind='search' width={24} height={24} color='#212121' />, 155 + text: '50 scripts found', 156 + }, 157 + { 158 + icon: <Icon kind='bug' width={24} height={24} color='#F3512E' />, 159 + text: '6 vulnerabilities in 4 packages', 160 + }, 161 + { 162 + icon: <Icon kind='duplicate' color='#F3812E' width={24} height={24} />, 163 + text: '12 duplicate packages', 164 + }, 165 + { 166 + icon: <Icon kind='outdated' color='#F1CE61' stroke='white' width={24} height={24} />, 167 + text: '18 outdated packages', 168 + }, 169 + ]; 170 + 123 171 // TODO: mock data, remove later 124 172 const keyWords = ['#moment', '#date', '#react', '#parse', '#fb', '#angular', '#vue', '#ember']; 125 173 skipped 3 lines 129 177 // TODO: mock data, remove later 130 178 const authors = ['gaearon', 'acdlite', 'sophiebits', 'sebmarkbage', 'zpao', 'trueadm', 'bvaughn']; 131 179 132 - const [selectedKeywords, setSelectedKeywords] = useState<string[]>([]); 133 - const [selectedProblems, setSelectedProblems] = useState<string[]>([]); 134 - const [selectedAuthors, setSelectedAuthors] = useState<string[]>([]); 135 - 136 - const handleFiltersChange = ( 137 - name: string, 138 - state: string[], 139 - setState: React.SetStateAction<any> 140 - ) => { 141 - const temp = [...state]; 142 - 143 - if (temp.includes(name)) { 144 - const filtered = temp.filter((item) => item !== name); 145 - setState(filtered); 146 - } else { 147 - temp.push(name); 148 - setState(temp); 149 - } 150 - }; 151 - 152 - const handleKeywordsChange = (name: string) => { 153 - handleFiltersChange(name, selectedKeywords, setSelectedKeywords); 154 - }; 155 - 156 - const handleProblemsChange = (name: string) => { 157 - handleFiltersChange(name, selectedProblems, setSelectedProblems); 158 - }; 159 - 160 - const handleAuthorsChange = (name: string) => { 161 - handleFiltersChange(name, selectedAuthors, setSelectedAuthors); 162 - }; 163 - 164 - const resetFilters = () => { 165 - setSelectedKeywords([]); 166 - setSelectedProblems([]); 167 - setSelectedAuthors([]); 168 - }; 169 - 170 - const isChanged = 171 - selectedKeywords.length > 0 || selectedProblems.length > 0 || selectedAuthors.length > 0; 172 - 173 180 return ( 174 181 <> 175 - <Header> 176 - <SearchBar value='pinterest.com/blog/%D0%92%D092%D092%D092%/dFD092fg092%D092%/dFD092/blog/%D0%92%D092%D092%D092%/dFD092fg092%D092%/dFD092f' /> 177 - </Header> 182 + {pageLoading && ( 183 + <LoadingBar 184 + ref={loadingRef} 185 + color='linear-gradient(90deg, #2638D9 0%, #B22AF2 100%)' 186 + height={4} 187 + shadow={false} 188 + transitionTime={600} 189 + loaderSpeed={600} 190 + /> 191 + )} 192 + 193 + <DefaultHeader showSearch /> 178 194 179 195 <Container> 180 196 <div className={styles.searchResults}> 181 - <SearchedResource 182 - image='https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg' 183 - name='pinterest.com' 184 - totalPackages={6} 185 - lastScanDate='21 feb in 21:30' 186 - /> 187 - 188 - <aside className={styles.sidebar}> 189 - <div className={styles.sidebarItem}> 190 - <div className={styles.meta}> 191 - <div className={styles.metaItem}> 192 - <span className={styles.metaIcon}> 193 - <Icon kind='weight' width={24} height={24} /> 194 - </span> 195 - <span className={styles.metaText}>159 kb webpack bundle size</span> 196 - </div> 197 - <div className={styles.metaItem}> 198 - <span className={styles.metaIcon}> 199 - <Icon kind='search' width={24} height={24} color='#212121' /> 200 - </span> 201 - <span className={styles.metaText}>50 scripts found</span> 202 - </div> 203 - <div className={styles.metaItem}> 204 - <span className={styles.metaIcon}> 205 - <Icon kind='bug' width={24} height={24} color='#F3512E' /> 206 - </span> 207 - <span className={styles.metaText}>6 vulnerabilities in 4 packages</span> 208 - </div> 209 - <div className={styles.metaItem}> 210 - <span className={styles.metaIcon}> 211 - <Icon kind='duplicate' color='#F3812E' width={24} height={24} /> 212 - </span> 213 - <span className={styles.metaText}>12 duplicate packages</span> 214 - </div> 215 - <div className={styles.metaItem}> 216 - <span className={styles.metaIcon}> 217 - <Icon kind='outdated' color='#F1CE61' stroke='white' width={24} height={24} /> 218 - </span> 219 - <span className={styles.metaText}>18 outdated packages</span> 220 - </div> 221 - </div> 222 - </div> 223 - 224 - <div className={styles.sidebarItem}> 225 - <SidebarCategory 226 - keywordsList={keyWords} 227 - selectedKeywords={selectedKeywords} 228 - selectHandler={handleKeywordsChange} 229 - renderComponent='chip' 230 - searchable 197 + <div className={styles.searchResultsResource}> 198 + {loading ? ( 199 + <SearchedResourceSkeleton 200 + image='https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg' 201 + name='pinterest.com' 231 202 /> 232 - </div> 233 - 234 - <div className={styles.sidebarItem}> 235 - <SidebarCategory 236 - keywordsList={vulnerabilities} 237 - selectedKeywords={selectedProblems} 238 - selectHandler={handleProblemsChange} 239 - renderComponent='checkbox' 203 + ) : ( 204 + <SearchedResource 205 + image='https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg' 206 + name='pinterest.com' 207 + totalPackages={6} 208 + lastScanDate='21 feb in 21:30' 240 209 /> 241 - </div> 210 + )} 211 + </div> 242 212 243 - <div className={styles.sidebarItem }> 244 - <SidebarCategory 245 - keywordsList={authors} 246 - selectedKeywords={selectedAuthors} 247 - selectHandler={handleAuthorsChange} 248 - renderComponent='person' 249 - searchable 250 - /> 251 - </div> 213 + <div className={styles.searchResultsSidebar }> 214 + <SearchResultsSidebar 215 + metaItems={metaItems} 216 + keyWords={keyWords} 217 + vulnerabilities={vulnerabilities} 218 + authors={authors} 219 + loading={loading} 220 + /> 221 + </div> 252 222 253 - {isChanged && ( 254 - <div className={styles.sidebarItem}> 255 - <Button variant='secondary' size='small' onClick={resetFilters}> 256 - Reset filters 257 - </Button> 258 - </div> 223 + <div className={styles.packages}> 224 + {loading ? ( 225 + <PackagePreviewSkeleton /> 226 + ) : ( 227 + <PackagePreview name='@team-griffin/react-heading-section' version='3.0.0 - 4.16.4' /> 259 228 )} 260 - </aside> 261 229 262 - <div className={styles.packages}> 263 - <PackagePreview name='@team-griffin/react-heading-section' version='3.0.0 - 4.16.4' /> 264 - <PackagePreview 265 - name='@team-griffin/react-heading-section@ team - griffin / react - heading -section ' 266 - version='3.0.0 - 4.16.4' 267 - /> 230 + {loading ? ( 231 + <PackagePreviewSkeleton /> 232 + ) : ( 233 + < PackagePreview name='@team-griffin/react-heading-section' version = ' 3 . 0 . 0 - 4 . 16 . 4 ' / > 234 + )} 268 235 </div> 269 236 </div> 270 237 238 + {/* TODO: Trying to fit separate domain entities within a single component seems like burden. 239 + Feels like these <CardList/>'s should be separate components. */} 271 240 <CardGroups> 272 241 <CardGroup title='Similar sites'> 273 - <CardList cards={similarCards} /> 242 + { loading ? < CardListSkeleton / > : <CardList cards={similarCards} />} 274 243 </CardGroup> 275 244 276 245 <CardGroup title='Popular packages'> 277 - <CardList cards={popularPackages} /> 246 + { loading ? < CardListSkeleton / > : <CardList cards={popularPackages} />} 278 247 </CardGroup> 279 248 </CardGroups> 280 249 </Container> skipped 6 lines