skipped 3 lines 4 4 import Chip from '../Chip/Chip'; 5 5 import clsx from 'clsx'; 6 6 import ChipGroup from '../ChipGroup/ChipGroup'; 7 - import SitesList, { Site } from '../SitesList/SitesList'; 7 + import SitesList from '../SitesList/SitesList'; 8 8 import { CSSTransition } from 'react-transition-group'; 9 9 import Button from '../Button/Button'; 10 - import { formatNumber } from 'utils/helpers'; 11 10 import { 12 11 LicenceSkeleton, 13 12 LinksSkeleton, 14 - PopularitySkeleton, 15 - PopularityVersionSkeleton, 16 13 RatingSkeleton, 17 14 ScriptSkeleton, 18 15 } from './PackagePreviewSkeleton'; 16 + import ProblemBadge from '../ProblemBadge/ProblemBadge'; 17 + import { ChipGroupSkeleton } from '../ChipGroup/ChipGroupSkeleton'; 18 + import { SitesListSkeleton } from '../SitesList/SitesListSkeleton'; 19 + import BarChart from '../BarChart/BarChart'; 20 + import BarChartSkeleton from '../BarChart/BarChartSkeleton'; 21 + import { formatNumber } from 'utils/helpers'; 22 + import Hint from '../Tooltip/Hint'; 23 + 24 + type Problem = 'vulnerabilities' | 'duplicate' | 'outdated'; 25 + 26 + type ExternalLink = { 27 + href: string; 28 + kind: 'repository' | 'link' | 'npm'; 29 + linkText?: string; 30 + }; 19 31 20 32 type Props = { 21 33 name: string; 22 34 version: string; 35 + desc: string; 36 + problems?: Problem[]; 37 + keywords: string[]; 38 + author: { 39 + name: string; 40 + image: string; 41 + }; 23 42 opened?: boolean; 24 43 detailsLoading?: boolean; 25 44 }; 26 45 27 46 // TODO: refactor this (decomposition, props, memoization, etc) 28 - export default function PackagePreview({ name , version , opened , detailsLoading = false } : Props ) { 47 + export default function PackagePreview({ 48 + name, 49 + version, 50 + desc, 51 + problems, 52 + keywords, 53 + author, 54 + opened, 55 + detailsLoading = false, 56 + }: Props) { 29 57 const [open, setOpen] = useState<boolean>(opened ?? false); 30 58 const [packageDetailsLoading, setPackageDetailsLoading] = useState<boolean>(detailsLoading); 31 59 32 - // TODO: mock data, remove later 33 - const sites: Site[] = [ 34 - { 35 - id: '123', 36 - image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 37 - name: 'pinterest.com', 38 - packagesCount: 151, 39 - }, 40 - { 41 - id: '456', 42 - image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 43 - name: 'pinterest.com', 44 - packagesCount: 151, 45 - }, 46 - { 47 - id: '789', 48 - image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 49 - name: 'pinterest.com', 50 - packagesCount: 151, 51 - }, 52 - { 53 - id: '1231', 54 - image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 55 - name: 'pinterest.com', 56 - packagesCount: 151, 57 - }, 58 - { 59 - id: '12321', 60 - image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 61 - name: 'pinterest.com', 62 - packagesCount: 151, 63 - }, 64 - { 65 - id: '123123', 66 - image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 67 - name: 'pinterest.com', 68 - packagesCount: 151, 69 - }, 70 - { 71 - id: '12123132', 72 - image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 73 - name: 'pinterest.com', 74 - packagesCount: 151, 75 - }, 76 - ]; 77 - 78 - // TODO: mock data, remove later 79 - const modules = [ 80 - { 81 - fill: '100%', 82 - uses: 89912, 83 - moduleVersion: '21.3.0', 84 - }, 85 - { 86 - fill: '90%', 87 - uses: 67111, 88 - moduleVersion: '18.2.0', 89 - }, 90 - { 91 - fill: '80%', 92 - uses: 44212, 93 - moduleVersion: '20.1.0', 94 - }, 95 - { 96 - fill: '70%', 97 - uses: 41129, 98 - moduleVersion: '18.0.0', 99 - }, 100 - { 101 - fill: '60%', 102 - uses: 40465, 103 - moduleVersion: '19.11.2', 104 - }, 105 - { 106 - fill: '50%', 107 - uses: 38907, 108 - moduleVersion: '8.1.2', 109 - bug: true, 110 - }, 111 - ]; 112 - 113 60 const toggleOpen = () => { 114 61 if (open) { 115 62 setOpen(false); skipped 2 lines 118 65 setPackageDetailsLoading(true); 119 66 120 67 // FIXME: just for demo purposes 121 - setTimeout(() => setPackageDetailsLoading(false), 60000 ); 68 + setTimeout(() => setPackageDetailsLoading(false), 4000 ); 122 69 } 123 70 }; 124 71 72 + // TODO: Mock API data, remove later 73 + const externalLinks: ExternalLink[] = [ 74 + { kind: 'repository', href: 'https://github.com/facebook/react/', linkText: 'Repository' }, 75 + { kind: 'link', href: 'https://reactjs.org/', linkText: 'Homepage' }, 76 + { kind: 'npm', href: 'https://www.npmjs.com/package/react' }, 77 + ]; 78 + 79 + // TODO: Mock API data, remove later 80 + const loadedData = { 81 + script: '/rsrc.php/v3id044/yu/l/en_US/yD2XaVkWQHO.js?_nc_x=Ij3Wp8lg5Kz', 82 + license: { 83 + title: 'MIT license', 84 + subtitle: 'freely distributable', 85 + }, 86 + rating: { 87 + place: 385, 88 + rankingDelta: -4, 89 + out: 12842, 90 + }, 91 + dependencies: ['art', 'create-react-class', 'loose-envify', 'scheduler'], 92 + packages: [ 93 + { 94 + fill: 1, 95 + uses: 89912, 96 + moduleVersion: '21.3.0', 97 + }, 98 + { 99 + fill: 0.8, 100 + uses: 67111, 101 + moduleVersion: '18.2.0', 102 + highlighted: true, 103 + }, 104 + { 105 + fill: 0.7, 106 + uses: 44212, 107 + moduleVersion: '20.1.0', 108 + }, 109 + { 110 + fill: 0.6, 111 + uses: 41129, 112 + moduleVersion: '18.0.0', 113 + }, 114 + { 115 + fill: 0.5, 116 + uses: 40465, 117 + moduleVersion: '19.11.2', 118 + }, 119 + { 120 + fill: 0.4, 121 + uses: 38907, 122 + moduleVersion: '8.1.2', 123 + vulnerabilities: true, 124 + }, 125 + ], 126 + sites: [ 127 + { 128 + id: '123', 129 + image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 130 + name: 'pinterest.com', 131 + packagesCount: 151, 132 + }, 133 + { 134 + id: '456', 135 + image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 136 + name: 'pinterest.com', 137 + packagesCount: 151, 138 + }, 139 + { 140 + id: '789', 141 + image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 142 + name: 'pinterest.com', 143 + packagesCount: 151, 144 + }, 145 + { 146 + id: '1231', 147 + image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 148 + name: 'pinterest.com', 149 + packagesCount: 151, 150 + }, 151 + { 152 + id: '12321', 153 + image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 154 + name: 'pinterest.com', 155 + packagesCount: 151, 156 + }, 157 + { 158 + id: '123123', 159 + image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 160 + name: 'pinterest.com', 161 + packagesCount: 151, 162 + }, 163 + { 164 + id: '12123132', 165 + image: 'https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg', 166 + name: 'pinterest.com', 167 + packagesCount: 151, 168 + }, 169 + ], 170 + links: externalLinks, 171 + }; 172 + 173 + const { script, license, rating, dependencies, packages, sites, links } = loadedData; 174 + 125 175 return ( 126 176 <div className={clsx(styles.package, open && styles.open)}> 127 - <header className={styles.header}> 177 + <div className={styles.header}> 128 178 <div className={styles.top} onClick={toggleOpen}> 129 179 <div className={styles.title}> 130 180 <span className={styles.name}> 131 181 {name} <span className={styles.version}>{version}</span> 132 182 </span> 133 - <span className={styles.problems}> 134 - <Chip 135 - variant='vulnerability' 136 - size='badge' 137 - icon={<Icon kind='bug' width={24} height={24} color='white' />} 138 - > 139 - Vulnerabilities 140 - </Chip> 141 - <Chip 142 - variant='duplicate' 143 - size='badge' 144 - icon={<Icon kind='duplicate' width={24} height={24} color='white' />} 145 - > 146 - Duplicate 147 - </Chip> 148 - <Chip 149 - variant='outdated' 150 - size='badge' 151 - icon={ 152 - <Icon kind='outdated' width={24} height={24} color='white' stroke='#F1CE61' /> 153 - } 154 - > 155 - Outdated 156 - </Chip> 157 - </span> 183 + 184 + {problems && ( 185 + <span className={styles.problems}> 186 + {problems.map((problem) => ( 187 + <ProblemBadge key={problem} problem={problem} /> 188 + ))} 189 + </span> 190 + )} 158 191 </div> 159 192 160 193 <button type='button' className={styles.arrowWrapper} onClick={toggleOpen}> 161 - {/* FIXME: requires different smaller svg icon for mobile and makes button 24x24 */} 162 - {/* which is probably not optimal UX */} 163 194 <Icon kind='arrowDown' width={14} height={8} color='#8E8AA0' className={styles.arrow} /> 164 195 </button> 165 196 </div> 166 197 167 - <div className={styles.desc}> 168 - The Lodash library exported as ES modules. Generated using lodash-cli 169 - </div> 170 - </header> 198 + <div className={styles.desc}>{ desc } < / div > 199 + </div> 171 200 172 201 <CSSTransition 173 202 in={open} skipped 13 lines 187 216 <ScriptSkeleton /> 188 217 ) : ( 189 218 <a href='#' className={styles.statLink} target='_blank' rel='noreferrer'> 190 - /rsrc.php/v3id044/yu/l/en_US/yD2XaVkWQHO.js?_nc_x=Ij3Wp8lg5Kz 219 + {script} 191 220 </a> 192 221 )} 193 222 </div> skipped 8 lines 202 231 <LicenceSkeleton /> 203 232 ) : ( 204 233 <> 205 - <div className={styles.statTitle}>MIT license</div> 206 - <div className={styles.statSubtitle}>freely distributable </div> 234 + <div className={styles.statTitle}>{ license. title } </div> 235 + <div className={styles.statSubtitle}>{ license . subtitle } </div> 207 236 </> 208 237 )} 209 238 </div> skipped 2 lines 212 241 <div className={styles.statHeader}> 213 242 <Icon kind='rating' color='#8E8AA0' className={styles.statIcon} /> 214 243 Rating 244 + <span className={styles.statTooltip}> 245 + <Hint text='Rating based on our service' /> 246 + </span> 215 247 </div> 216 248 {packageDetailsLoading ? ( 217 249 <RatingSkeleton /> 218 250 ) : ( 219 - // TODO: What about adding a rankingDelta prop and deciding on class name based on number's sign? 220 251 <> 221 252 <div className={styles.statTitle}> 222 - 385 223 - {/* or: <div className={clsx(styles.statRating, styles.statRatingRed)}> */} 224 - <div className={clsx(styles.statRating, styles.statRatingGreen)}> 253 + {rating.place} 254 + 255 + <div 256 + className={clsx( 257 + styles.statRating, 258 + rating.rankingDelta > 0 ? styles.statRatingGreen : styles.statRatingRed 259 + )} 260 + > 225 261 <Icon 226 262 kind='ratingArrow' 227 263 width={12} 228 264 height={12} 229 265 className={styles.statRatingArrow} 230 266 /> 231 - +4 267 + {rating.rankingDelta} 232 268 </div> 233 269 </div> 234 - <div className={styles.statSubtitle}>out of 12 842 </div> 270 + <div className={styles.statSubtitle}>out of { formatNumber ( rating . out ) } </div> 235 271 </> 236 272 )} 237 273 </div> skipped 1 lines 239 275 <div className={clsx(styles.stat, styles.statListItemLarge)}> 240 276 <div className={styles.statHeader}> 241 277 <Icon kind='dependency' color='#8E8AA0' className={styles.statIcon} /> 242 - {!packageDetailsLoading && 4} Dependency 278 + Dependencies 243 279 </div> 244 - <ChipGroup 245 - chips={['art', 'create-react-class', 'loose-envify', 'scheduler']} 246 - fontSize='small' 247 - loading={packageDetailsLoading} 248 - /> 280 + {packageDetailsLoading ? ( 281 + <ChipGroupSkeleton /> 282 + ) : ( 283 + <ChipGroup> 284 + {dependencies.map((dependency) => ( 285 + <Chip size='medium' fontSize='small' font='monospace'> 286 + {dependency} 287 + </Chip> 288 + ))} 289 + </ChipGroup> 290 + )} 249 291 </div> 250 292 </div> 251 293 skipped 4 lines 256 298 </div> 257 299 258 300 <div className={styles.popularity}> 259 - {modules.map(({ fill, uses, moduleVersion, bug }) => ( 260 - <div className={styles.popularityItemWrapper}> 261 - <div className={styles.popularityItem}> 262 - {packageDetailsLoading ? ( 263 - // TODO: We should be on lookout for these deoptimizations, 264 - // this should definitely be a component / top-level const. 265 - <div 266 - className={clsx(styles.popularityFill, styles.popularityFillSkeleton)} 267 - style={{ height: fill }} 268 - > 269 - <PopularitySkeleton /> 270 - </div> 271 - ) : ( 272 - <div className={styles.popularityFill} style={{ height: fill }}> 273 - {formatNumber(uses)} 274 - </div> 275 - )} 276 - </div> 277 - 278 - {packageDetailsLoading ? ( 279 - <PopularityVersionSkeleton /> 280 - ) : ( 281 - <div className={styles.popularityVersion}> 282 - {moduleVersion} 283 - {bug && ( 284 - <Icon 285 - kind='bugOutlined' 286 - color='#212121' 287 - className={styles.popularityVersionIcon} 288 - /> 289 - )} 290 - </div> 291 - )} 292 - </div> 293 - ))} 301 + {packageDetailsLoading ? <BarChartSkeleton /> : <BarChart bars={packages} />} 294 302 </div> 295 303 </div> 296 304 skipped 2 lines 299 307 <div className={styles.stat}> 300 308 <div className={styles.statHeader}>Used on</div> 301 309 302 - <SitesList 303 - sites={sites} 304 - className={styles.usedOnList} 305 - loading={packageDetailsLoading} 306 - /> 310 + {packageDetailsLoading ? ( 311 + <SitesListSkeleton className={styles.usedOnList} /> 312 + ) : ( 313 + <SitesList sites={sites} className={styles.usedOnList} /> 314 + )} 307 315 </div> 308 316 309 317 <div className={styles.actions}> skipped 1 lines 311 319 {packageDetailsLoading ? ( 312 320 <LinksSkeleton /> 313 321 ) : ( 314 - <> 315 - <a href='#' className={styles.link} target='_blank' rel='noreferrer'> 316 - <Icon kind='repository' color='#212121' className={styles.linkIcon} /> 317 - Repository 318 - </a> 319 - 320 - <a href='#' className={styles.link} target='_blank' rel='noreferrer'> 321 - <Icon kind='link' color='#212121' className={styles.linkIcon} /> 322 - Homepage 323 - </a> 324 - 325 - <a href='#' className={styles.link} target='_blank' rel='noreferrer'> 322 + links.map(({ href, kind, linkText }) => ( 323 + <a 324 + key={href} 325 + href={href} 326 + className={styles.link} 327 + target='_blank' 328 + rel='noreferrer' 329 + > 326 330 <Icon 327 - kind='npm' 328 - width={32} 329 - height={32} 331 + kind={kind} 332 + width={kind ! = = ' npm ' ? 16 : 32} 333 + height={kind ! = = ' npm ' ? 16 : 32} 330 334 color='#212121' 331 335 className={styles.linkIcon} 332 336 /> 337 + {linkText} 333 338 </a> 334 - </> 339 + )) 335 340 )} 336 341 </div> 337 342 skipped 3 lines 341 346 </div> 342 347 </CSSTransition> 343 348 344 - <footer className={styles.footer}> 349 + <div className={styles.footer}> 345 350 <div className={styles.tags}> 346 - <a href='#' className={styles.tag}> 347 - #moment 348 - </a> 349 - <a href='#' className={styles.tag}> 350 - #date 351 - </a> 352 - <a href='#' className={styles.tag}> 353 - #time 354 - </a> 355 - <a href='#' className={styles.tag}> 356 - #parse 357 - </a > 358 - <a href='#' className={styles.tag}> 359 - #format 360 - </a> 361 - <Chip variant='info' size='medium' fontWeight='semiBold'> 362 - +45 363 - </Chip> 351 + {/* TODO: not sure how to conditionally render maximum number of keywords (e.g. 5 for 352 + desktop, 3/4 for tablet, 2 for mobile) based on viewport and update rest number 353 + of keywords beyond current maximum in Chip */} 354 + {keywords.slice(0, 5).map((keyword) => ( 355 + <a key={keyword} href='#' className={styles.tag}> 356 + {keyword} 357 + </a> 358 + ))} 359 + {keywords.slice(5).length > 0 && ( 360 + <Chip variant='info' size='medium' fontWeight='semiBold'> 361 + +{keywords.slice(5).length} 362 + </Chip > 363 + )} 364 364 </div> 365 365 366 366 <div className={styles.author}> 367 - <span className={styles.authorName}>jdalton </span> 368 - <img className={styles.authorImage} src=' https : / / via . placeholder .com / 36 ' alt='' /> 367 + <span className={styles.authorName}>{ author . name } </span> 368 + <img className={styles.authorImage} src={ author .image } alt='' /> 369 369 </div> 370 - </footer> 370 + </div> 371 371 </div> 372 372 ); 373 373 } skipped 1 lines