Projects STRLCPY gradejs Commits 9e716a54
🤬
  • wip: add interactivity to sidebar filters and styling update for related components, useMemo for Header links

  • Loading...
  • Dmitry Shakun committed 2 years ago
    9e716a54
    1 parent f160dec8
  • packages/web/src/assets/icons/sprite/search.svg
  • ■ ■ ■ ■ ■
    packages/web/src/components/layouts/SearchResults/SearchResults.module.scss
    skipped 32 lines
    33 33   
    34 34  .packages {
    35 35   grid-column: 2 / -1;
     36 + display: grid;
     37 + grid-gap: 16px;
     38 + align-items: start;
     39 + grid-template-rows: min-content;
    36 40   
    37 41   @include mobile-and-tablet {
    38 42   grid-column: 1;
    skipped 22 lines
    61 65   }
    62 66  }
    63 67   
    64  -.sidebarItemTop {
    65  - display: flex;
    66  - align-items: center;
    67  - justify-content: space-between;
    68  - margin-bottom: 16px;
    69  -}
    70  - 
    71  -.sidebarItemTitle {
    72  - font-weight: 500;
    73  - font-size: 19px;
    74  - line-height: 26px;
    75  -}
    76  - 
    77  -.sidebarItemCounter {
    78  - margin-left: 8px;
    79  -}
    80  - 
    81  -.sidebarItemAction {
    82  - display: flex;
    83  -}
    84  - 
    85 68  .meta {
    86 69   display: grid;
    87 70   grid-gap: 20px;
    skipped 27 lines
    115 98   font-weight: 500;
    116 99  }
    117 100   
    118  -.viewAll {
    119  - margin-top: 4px;
    120  - cursor: pointer;
    121  - font-weight: 500;
    122  - color: $blue-accent;
    123  - transition: opacity $transition-duration $transition-timing-function;
    124  - 
    125  - &:hover {
    126  - opacity: 0.6;
    127  - }
    128  -}
    129  - 
    130  -.checkboxGroup {
    131  - display: grid;
    132  - grid-gap: 18px;
    133  -}
    134  - 
    135  -.authors {
    136  - display: flex;
    137  - flex-wrap: wrap;
    138  -}
    139  - 
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/layouts/SearchResults/SearchResults.tsx
    1  -import React from 'react';
     1 +import React, { useState } from 'react';
    2 2  import styles from './SearchResults.module.scss';
    3 3  import Header from 'components/ui/Header/Header';
    4 4  import Footer from 'components/ui/Footer/Footer';
    5 5  import Container from 'components/ui/Container/Container';
    6 6  import { Icon } from '../../ui/Icon/Icon';
    7  -import ChipGroup from '../../ui/ChipGroup/ChipGroup';
    8 7  import PackagePreview from '../../ui/PackagePreview/PackagePreview';
    9 8  import SearchBar from '../../ui/SearchBar/SearchBar';
    10  -import Badge from '../../ui/Badge/Badge';
    11  -import Person from 'components/ui/Person/Person';
    12  -import Checkbox from '../../ui/Checkbox/Checkbox';
    13 9  import SearchedResource from '../../ui/SearchedResource/SearchedResource';
    14 10  import { CardProps } from '../../ui/Card/Card';
    15 11  import CardGroup from '../../ui/CardGroup/CardGroup';
    16 12  import CardList from '../../ui/CardList/CardList';
    17 13  import CardGroups from 'components/ui/CardGroups/CardGroups';
     14 +import SidebarCategory from '../../ui/SidebarCategory/SidebarCategory';
     15 +import { Button } from '../../ui';
    18 16   
    19 17  export default function SearchResults() {
    20 18   // TODO: mock date, remove later
    skipped 101 lines
    122 120   },
    123 121   ];
    124 122   
     123 + const keyWords = {
     124 + fullList: [
     125 + {
     126 + id: '#art',
     127 + name: '#art',
     128 + },
     129 + {
     130 + id: '#angular',
     131 + name: '#angular',
     132 + },
     133 + {
     134 + id: '#moment',
     135 + name: '#moment',
     136 + },
     137 + {
     138 + id: '#date',
     139 + name: '#date',
     140 + },
     141 + {
     142 + id: '#react',
     143 + name: '#react',
     144 + },
     145 + {
     146 + id: '#parse',
     147 + name: '#parse',
     148 + },
     149 + {
     150 + id: '#fb',
     151 + name: '#fb',
     152 + },
     153 + ],
     154 + featuredItems: ['#moment', '#date', '#react', '#parse', '#fb'],
     155 + };
     156 + 
     157 + const vulnerabilities = ['Vulnerabilities', 'Outdated', 'Duplicate'];
     158 + 
     159 + const authors = {
     160 + fullList: [
     161 + {
     162 + id: 'acdlite',
     163 + name: 'acdlite',
     164 + },
     165 + {
     166 + id: 'gaearon',
     167 + name: 'gaearon',
     168 + },
     169 + {
     170 + id: 'sophiebits',
     171 + name: 'sophiebits',
     172 + },
     173 + {
     174 + id: 'trueadm',
     175 + name: 'trueadm',
     176 + },
     177 + ],
     178 + featuredItems: ['acdlite', 'gaearon', 'sophiebits', 'trueadm'],
     179 + };
     180 + 
     181 + const [selectedKeywords, setSelectedKeywords] = useState<string[] | []>([]);
     182 + const [selectedProblems, setSelectedProblems] = useState<string[] | []>([]);
     183 + const [selectedAuthors, setSelectedAuthors] = useState<string[] | []>([]);
     184 + 
     185 + const handleFiltersChange = (
     186 + name: string,
     187 + state: string[] | [],
     188 + setState: React.SetStateAction<any>
     189 + ) => {
     190 + const temp = [...state];
     191 + 
     192 + if (temp.includes(name)) {
     193 + const filtered = temp.filter((item) => item !== name);
     194 + setState(filtered);
     195 + } else {
     196 + temp.push(name);
     197 + setState(temp);
     198 + }
     199 + };
     200 + 
     201 + const handleKeywordsChange = (name: string) => {
     202 + handleFiltersChange(name, selectedKeywords, setSelectedKeywords);
     203 + };
     204 + 
     205 + const handleProblemsChange = (name: string) => {
     206 + handleFiltersChange(name, selectedProblems, setSelectedProblems);
     207 + };
     208 + 
     209 + const handleAuthorsChange = (name: string) => {
     210 + handleFiltersChange(name, selectedAuthors, setSelectedAuthors);
     211 + };
     212 + 
     213 + const resetFilters = () => {
     214 + setSelectedKeywords([]);
     215 + setSelectedProblems([]);
     216 + setSelectedAuthors([]);
     217 + };
     218 + 
     219 + const isChanged =
     220 + selectedKeywords.length > 0 || selectedProblems.length > 0 || selectedAuthors.length > 0;
     221 + 
    125 222   return (
    126 223   <>
    127 224   <Header>
    skipped 20 lines
    148 245   </div>
    149 246   <div className={styles.metaItem}>
    150 247   <span className={styles.metaIcon}>
    151  - <Icon kind='search' width={24} height={24} />
     248 + <Icon kind='search' width={24} height={24} color='#212121' />
    152 249   </span>
    153 250   <span className={styles.metaText}>50 scripts found</span>
    154 251   </div>
    skipped 19 lines
    174 271   </div>
    175 272   
    176 273   <div className={styles.sidebarItem}>
    177  - <div className={styles.sidebarItemTop}>
    178  - <div className={styles.sidebarItemTitle}>Keywords</div>
    179  - <div className={styles.sidebarItemAction}>
    180  - <Icon kind='search' width={24} height={24} />
    181  - </div>
    182  - </div>
    183  - 
    184  - <ChipGroup chips={['#moment', '#date', '#react', '#parse', '#fb']} />
    185  - <span role='button' className={styles.viewAll}>
    186  - View All
    187  - </span>
     274 + <SidebarCategory
     275 + category={keyWords}
     276 + selectedKeywords={selectedKeywords}
     277 + selectHandler={handleKeywordsChange}
     278 + renderComponent='chip'
     279 + searchable
     280 + />
    188 281   </div>
    189 282   
    190 283   <div className={styles.sidebarItem}>
    191  - <div className={styles.sidebarItemTop}>
    192  - <div className={styles.sidebarItemTitle}>
    193  - Problem
    194  - <span className={styles.sidebarItemCounter}>
    195  - <Badge content={1} />
    196  - </span>
    197  - </div>
    198  - </div>
    199  - 
    200  - <div className={styles.checkboxGroup}>
    201  - <Checkbox label='Vulnerabilities' checked />
    202  - <Checkbox label='Outdated' />
    203  - <Checkbox label='Duplicate' />
    204  - </div>
     284 + <SidebarCategory
     285 + simpleCategory={vulnerabilities}
     286 + selectedKeywords={selectedProblems}
     287 + selectHandler={handleProblemsChange}
     288 + renderComponent='checkbox'
     289 + />
    205 290   </div>
    206 291   
    207 292   <div className={styles.sidebarItem}>
    208  - <div className={styles.sidebarItemTop}>
    209  - <div className={styles.sidebarItemTitle}>Authors</div>
    210  - <div className={styles.sidebarItemAction}>
    211  - <Icon kind='search' width={24} height={24} />
    212  - </div>
    213  - </div>
     293 + <SidebarCategory
     294 + category={authors}
     295 + selectedKeywords={selectedAuthors}
     296 + selectHandler={handleAuthorsChange}
     297 + renderComponent='person'
     298 + searchable
     299 + />
     300 + </div>
    214 301   
    215  - <div className={styles.authors}>
    216  - <Person image='https://via.placeholder.com/36' name='acdlite' checked />
    217  - <Person image='https://via.placeholder.com/36' name='gaearon' />
    218  - <Person image='https://via.placeholder.com/36' name='sophiebits' />
    219  - <Person image='https://via.placeholder.com/36' name='trueadm' />
     302 + {isChanged && (
     303 + <div className={styles.sidebarItem}>
     304 + <Button variant='secondary' size='small' onClick={resetFilters}>
     305 + Reset filters
     306 + </Button>
    220 307   </div>
    221  - 
    222  - <span role='button' className={styles.viewAll}>
    223  - View All
    224  - </span>
    225  - </div>
     308 + )}
    226 309   </aside>
    227 310   
    228 311   <div className={styles.packages}>
     312 + <PackagePreview
     313 + name='@team-griffin/react-heading-section@team-griffin/react-heading-section'
     314 + version='3.0.0 - 4.16.4'
     315 + />
    229 316   <PackagePreview
    230 317   name='@team-griffin/react-heading-section@team-griffin/react-heading-section'
    231 318   version='3.0.0 - 4.16.4'
    skipped 20 lines
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/ui/AvatarGroup/AvatarGroup.tsx
    skipped 11 lines
    12 12   <div className={styles.avatarsWrapper}>
    13 13   <div className={styles.avatars}>
    14 14   <div className={styles.avatarGroup}>
    15  - {avatarGroup.map((avatar) => (
    16  - <div key={avatar} className={styles.avatarItem}>
     15 + {avatarGroup.map((avatar, idx) => (
     16 + <div key={idx} className={styles.avatarItem}>
    17 17   <img src={avatar} className={styles.avatarImage} alt='' />
    18 18   </div>
    19 19   ))}
    skipped 8 lines
  • ■ ■ ■ ■ ■
    packages/web/src/components/ui/Badge/Badge.module.scss
    skipped 2 lines
    3 3  .badge {
    4 4   display: inline-flex;
    5 5   align-items: center;
    6  - padding: 0 8px;
     6 + justify-content: center;
     7 + padding: 0 6px;
    7 8   font-weight: 500;
    8 9   font-size: 14px;
    9 10   line-height: 22px;
    skipped 7 lines
  • ■ ■ ■ ■ ■
    packages/web/src/components/ui/Button/Button.module.scss
    1 1  @import '~styles/_vars.scss';
    2 2  @import '~styles/responsive.scss';
    3 3   
    4  -//.button {
    5  -// appearance: none;
    6  -// border-radius: 6px;
    7  -// border: none;
    8  -// outline: none;
    9  -// cursor: pointer;
    10  -// line-height: 140%;
    11  -// font-family: 'Inter', sans-serif;
    12  -// font-weight: 400;
    13  -// box-sizing: border-box;
    14  -//}
    15  -//
    16  -//// Sizes
    17  -//.big {
    18  -// padding: 16px 16px;
    19  -// font-size: 16px;
    20  -// line-height: 28px;
    21  -//
    22  -// @include mobile {
    23  -// font-size: 16px;
    24  -// line-height: 22px;
    25  -// }
    26  -//}
    27  -//
    28  -//.medium {
    29  -// padding: 12px 10px 12px;
    30  -// font-size: 14px;
    31  -// line-height: 18px;
    32  -//}
    33  -//
    34  -//// Variants
    35  -//.default {
    36  -// background: #fff;
    37  -// border: 1px solid #e6e6e6;
    38  -// color: #0f0f0f;
    39  -// transition: border-color 0.2s;
    40  -//
    41  -// &:enabled:hover {
    42  -// border-color: #a5a5a5;
    43  -// }
    44  -//
    45  -// &:enabled:active {
    46  -// border-color: #000000;
    47  -// }
    48  -//}
    49  -//
    50  -//.black {
    51  -// background-color: #0f0f0f;
    52  -// color: #fff;
    53  -//}
    54  -//
    55  -//.action {
    56  -// background: url('~assets/icons/arrow-right.svg') right 20px center / 20px 21px no-repeat;
    57  -// background-color: #0f0f0f;
    58  -// color: #fff;
    59  -// text-align: left;
    60  -// padding-right: 60px;
    61  -//}
    62  -//
    63  -//.black,
    64  -//.action {
    65  -// transition: background-color 0.2s;
    66  -//
    67  -// &:enabled:hover {
    68  -// background-color: #666666;
    69  -// }
    70  -// &:enabled:active {
    71  -// background-color: #a5a5a5;
    72  -// }
    73  -//}
    74  -//
    75  -//.action:disabled {
    76  -// background-color: #666666;
    77  -// background-image: url('~assets/loader-white.svg');
    78  -//}
    79  - 
    80 4  .button {
    81 5   position: relative;
    82 6   display: inline-flex;
    skipped 83 lines
    166 90   }
    167 91  }
    168 92   
     93 +.small {
     94 + font-size: 14px;
     95 + line-height: 20px;
     96 + padding: 10px 18px;
     97 +}
     98 + 
     99 +.secondary {
     100 + background-color: $gray-surface;
     101 + color: $black;
     102 + transition: background-color $transition-duration $transition-timing-function;
     103 + 
     104 + &:hover {
     105 + background-color: $gray-border;
     106 + }
     107 +}
     108 + 
  • ■ ■ ■ ■ ■
    packages/web/src/components/ui/Button/Button.tsx
    1  -/* eslint-disable react/button-has-type */
    2 1  import React, { MouseEventHandler } from 'react';
    3 2  import clsx from 'clsx';
    4 3  import styles from './Button.module.scss';
    skipped 2 lines
    7 6   className?: string;
    8 7   children: React.ReactNode;
    9 8   type?: React.ButtonHTMLAttributes<HTMLButtonElement>['type'];
    10  - variant?: 'default' | 'arrow';
    11  - size?: 'medium' | 'big';
     9 + variant?: 'default' | 'arrow' | 'secondary';
     10 + size?: 'small' | 'medium' | 'big';
    12 11   disabled?: boolean;
    13 12   onClick?: MouseEventHandler;
    14 13  };
    skipped 52 lines
  • ■ ■ ■ ■ ■
    packages/web/src/components/ui/Checkbox/Checkbox.tsx
    skipped 3 lines
    4 4  type Props = {
    5 5   checked?: boolean;
    6 6   label: string;
     7 + onChange?: React.ChangeEventHandler<HTMLInputElement>;
    7 8  };
    8 9   
    9  -export default function Checkbox({ checked, label }: Props) {
     10 +export default function Checkbox({ checked, label, onChange }: Props) {
    10 11   return (
    11 12   <label className={styles.checkbox}>
    12  - <input type='checkbox' className={styles.checkboxInput} checked={checked} />
     13 + <input
     14 + type='checkbox'
     15 + className={styles.checkboxInput}
     16 + checked={checked}
     17 + onChange={onChange}
     18 + />
    13 19   <span className={styles.checkboxName}>{label}</span>
    14 20   </label>
    15 21   );
    skipped 2 lines
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/ui/Chip/Chip.tsx
    skipped 11 lines
    12 12   fontWeight?: 400 | 500;
    13 13   fontSize?: 'small' | 'regular';
    14 14   icon?: React.ReactElement<IconProps>;
     15 + onClick?: React.MouseEventHandler<HTMLSpanElement>;
    15 16  };
    16 17   
    17 18  export default function Chip({
    skipped 5 lines
    23 24   fontWeight = 400,
    24 25   fontSize = 'regular',
    25 26   icon,
     27 + onClick,
    26 28  }: ChipProps) {
    27 29   return (
    28 30   <span
    skipped 6 lines
    35 37   fontSize === 'small' && styles.smallFont,
    36 38   className
    37 39   )}
     40 + onClick={onClick}
    38 41   >
    39 42   <span className={styles.icon}>{icon}</span>
    40 43   {children}
    skipped 4 lines
  • ■ ■ ■ ■ ■
    packages/web/src/components/ui/Header/Header.tsx
    1  -import React from 'react';
     1 +import React, { useMemo } from 'react';
    2 2  import styles from './Header.module.scss';
    3 3  import Container from '../Container/Container';
    4 4  import clsx from 'clsx';
    skipped 6 lines
    11 11  };
    12 12   
    13 13  export default function Header({ variant = 'default', children }: Props) {
     14 + const trackAboutClick = useMemo(() => {
     15 + return () => trackCustomEvent('ClickExternalLink', 'About');
     16 + }, []);
     17 + 
     18 + const trackAboutCommunity = useMemo(() => {
     19 + return () => trackCustomEvent('ClickExternalLink', 'Community');
     20 + }, []);
     21 + 
     22 + const trackAboutSourceCode = useMemo(() => {
     23 + return () => trackCustomEvent('ClickExternalLink', 'SourceCode');
     24 + }, []);
     25 + 
    14 26   return (
    15 27   <Container>
    16 28   <header className={clsx(styles.header, styles[variant])}>
    skipped 14 lines
    31 43   target='_blank'
    32 44   rel='noreferrer'
    33 45   className={styles.navLink}
    34  - onClick={() => trackCustomEvent('ClickExternalLink', 'About')}
     46 + onClick={trackAboutClick}
    35 47   >
    36 48   About
    37 49   </a>
    skipped 2 lines
    40 52   target='_blank'
    41 53   rel='noreferrer'
    42 54   className={styles.navLink}
    43  - onClick={() => trackCustomEvent('ClickExternalLink', 'Community')}
     55 + onClick={trackAboutCommunity}
    44 56   >
    45 57   Community
    46 58   </a>
    skipped 2 lines
    49 61   target='_blank'
    50 62   rel='noreferrer'
    51 63   className={styles.navLink}
    52  - onClick={() => trackCustomEvent('ClickExternalLink', 'SourceCode')}
     64 + onClick={trackAboutSourceCode}
    53 65   >
    54 66   <Icon
    55 67   kind='githubLogo'
    skipped 12 lines
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/ui/Header/HeaderHomePage.stories.tsx
    skipped 10 lines
    11 11   default: 'dark',
    12 12   },
    13 13   },
     14 + argTypes: {
     15 + variant: {
     16 + options: ['homepage', 'default'],
     17 + control: { type: 'radio' },
     18 + },
     19 + },
    14 20  } as ComponentMeta<typeof Header>;
    15 21   
    16 22  export const HomePage: ComponentStory<typeof Header> = () => <Header variant='homepage' />;
    skipped 1 lines
  • ■ ■ ■ ■ ■
    packages/web/src/components/ui/Person/Person.module.scss
    skipped 6 lines
    7 7   margin-bottom: 10px;
    8 8   
    9 9   &:not(:last-child) {
    10  - margin-right: 16px;
     10 + margin-right: 10px;
    11 11   }
    12 12   
    13 13   &:hover {
    skipped 11 lines
    25 25   display: flex;
    26 26   flex-shrink: 0;
    27 27   margin-right: 6px;
    28  - max-width: 36px;
     28 + width: 36px;
     29 + height: 36px;
    29 30  }
    30 31   
    31 32  .personImage {
    skipped 11 lines
    43 44   margin-left: 6px;
    44 45  }
    45 46   
     47 +.personCheckPlaceholder {
     48 + display: inline-flex;
     49 + margin-left: 6px;
     50 + width: 12px;
     51 + height: 10px;
     52 +}
     53 + 
  • ■ ■ ■ ■ ■
    packages/web/src/components/ui/Person/Person.tsx
    skipped 6 lines
    7 7   image?: string;
    8 8   name: string;
    9 9   checked?: boolean;
     10 + className?: string;
     11 + onClick?: React.MouseEventHandler<HTMLDivElement>;
    10 12  };
    11 13   
    12  -export default function Person({ image, name, checked }: Props) {
     14 +export default function Person({ image, name, checked, className, onClick }: Props) {
    13 15   return (
    14  - <div className={clsx(styles.person, checked && styles.personActive)}>
     16 + <div
     17 + className={clsx(styles.person, checked && styles.personActive, className)}
     18 + onClick={onClick}
     19 + >
    15 20   <div className={styles.personImageWrapper}>
    16 21   <img className={styles.personImage} src={image} alt='' />
    17 22   </div>
    18 23   <div className={styles.personText}>
    19 24   <span className={styles.personName}>{name}</span>
    20  - {checked && (
     25 + {checked ? (
    21 26   <span className={styles.personCheck}>
    22 27   <Icon kind='check' width={12} height={10} color='#212121' />
    23 28   </span>
     29 + ) : (
     30 + <span className={styles.personCheckPlaceholder} />
    24 31   )}
    25 32   </div>
    26 33   </div>
    skipped 3 lines
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/ui/SearchBar/SearchBar.module.scss
    skipped 35 lines
    36 36   
    37 37  .submit,
    38 38  .clear {
     39 + flex-shrink: 0;
    39 40   cursor: pointer;
    40 41   position: absolute;
    41 42   top: 16px;
    42 43   right: 16px;
    43 44   width: 24px;
    44 45   height: 24px;
     46 + padding: 0;
    45 47   border: none;
    46 48   background: transparent;
    47 49   user-select: none;
    skipped 10 lines
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/ui/SidebarCategory/SidebarCategory.module.scss
     1 +@import '~styles/_vars.scss';
     2 + 
     3 +.category {
     4 +}
     5 + 
     6 +.sidebarItemTop {
     7 + display: flex;
     8 + align-items: center;
     9 + justify-content: space-between;
     10 + margin-bottom: 16px;
     11 +}
     12 + 
     13 +.sidebarItemTitle {
     14 + font-weight: 500;
     15 + font-size: 19px;
     16 + line-height: 26px;
     17 +}
     18 + 
     19 +.sidebarItemCounter {
     20 + margin-left: 8px;
     21 +}
     22 + 
     23 +.sidebarItemAction {
     24 + cursor: pointer;
     25 + display: flex;
     26 +}
     27 + 
     28 +.selectedCounter {
     29 + margin-left: 8px;
     30 +}
     31 + 
     32 +.toggleView {
     33 + display: block;
     34 + margin-top: 4px;
     35 + cursor: pointer;
     36 + font-weight: 500;
     37 + color: $blue-accent;
     38 + transition: opacity $transition-duration $transition-timing-function;
     39 + 
     40 + &:hover {
     41 + opacity: 0.6;
     42 + }
     43 +}
     44 + 
     45 +.sidebarChip {
     46 + margin-bottom: 8px;
     47 + 
     48 + &:not(:last-child) {
     49 + margin-right: 8px;
     50 + }
     51 +}
     52 + 
     53 +.sidebarChipActive {
     54 + background-color: $black;
     55 + color: $white;
     56 + 
     57 + &:hover {
     58 + background-color: rgba($black, 0.6);
     59 + }
     60 +}
     61 + 
     62 +.authors {
     63 + display: flex;
     64 + flex-wrap: wrap;
     65 +}
     66 + 
     67 +.checkboxGroup {
     68 + display: grid;
     69 + grid-gap: 18px;
     70 +}
     71 + 
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/ui/SidebarCategory/SidebarCategory.tsx
     1 +import React, { useState, useEffect } from 'react';
     2 +import styles from './SidebarCategory.module.scss';
     3 +import { Icon } from '../Icon/Icon';
     4 +import clsx from 'clsx';
     5 +import Badge from '../Badge/Badge';
     6 +import Chip from '../Chip/Chip';
     7 +import Person from '../Person/Person';
     8 +import Checkbox from '../Checkbox/Checkbox';
     9 +import SidebarCategorySearch from '../SidebarCategorySearch/SidebarCategorySearch';
     10 + 
     11 +type listItem = {
     12 + id: string;
     13 + name: string;
     14 +};
     15 + 
     16 +type GroupItem = {
     17 + group: string;
     18 + children: listItem[];
     19 +};
     20 + 
     21 +type Group = {
     22 + [key: string]: GroupItem;
     23 +};
     24 + 
     25 +type Props = {
     26 + category?: {
     27 + fullList: listItem[];
     28 + featuredItems: string[];
     29 + };
     30 + simpleCategory?: string[];
     31 + selectedKeywords: string[] | [];
     32 + selectHandler: (name: string) => void;
     33 + renderComponent: 'chip' | 'checkbox' | 'person';
     34 + searchable?: boolean;
     35 +};
     36 + 
     37 +export default function SidebarCategory({
     38 + category,
     39 + simpleCategory,
     40 + selectedKeywords,
     41 + selectHandler,
     42 + renderComponent,
     43 + searchable,
     44 +}: Props) {
     45 + const [open, setOpen] = useState<boolean>(false);
     46 + const [searchValue, setSearchValue] = useState<string>('');
     47 + const [list, setList] = useState<GroupItem[] | []>([]);
     48 + 
     49 + const sortAndGroupList = (unorderedList: listItem[], value: string): GroupItem[] => {
     50 + const filteredList = unorderedList.filter((item) => item.name.includes(value));
     51 + const sortedList = filteredList.sort((a: listItem, b: listItem) =>
     52 + a.name.localeCompare(b.name)
     53 + );
     54 + 
     55 + const groups = sortedList.reduce((r: Group, e) => {
     56 + const group = e.name.includes('#') ? e.name[1] : e.name[0];
     57 + if (!r[group]) r[group] = { group, children: [e] };
     58 + else r[group].children.push(e);
     59 + return r;
     60 + }, {});
     61 + 
     62 + return Object.values(groups);
     63 + };
     64 + 
     65 + const toggleOpen = () => {
     66 + setOpen(!open);
     67 + };
     68 + 
     69 + const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
     70 + setSearchValue(e.target.value);
     71 + };
     72 + 
     73 + const clearInput = () => {
     74 + setSearchValue('');
     75 + };
     76 + 
     77 + let combinedList, chips, checkboxes, people;
     78 + if (searchable && category) {
     79 + const { featuredItems, fullList } = category;
     80 + 
     81 + // FIXME: not sure that this is optimal UX, because when we're selecting item from featured list,
     82 + // it jumps to first half of the list with other previously selected items, maybe it's fine though
     83 + combinedList = fullList &&
     84 + featuredItems && [...new Set([...selectedKeywords, ...featuredItems])];
     85 + 
     86 + useEffect(() => {
     87 + const filteredList = sortAndGroupList(fullList, searchValue);
     88 + setList(filteredList);
     89 + }, [searchValue]);
     90 + 
     91 + chips = combinedList?.map((chip) => (
     92 + <Chip
     93 + key={chip}
     94 + className={clsx(
     95 + styles.sidebarChip,
     96 + selectedKeywords.includes(chip) && styles.sidebarChipActive
     97 + )}
     98 + onClick={() => selectHandler(chip)}
     99 + size='medium'
     100 + font='monospace'
     101 + >
     102 + {chip}
     103 + </Chip>
     104 + ));
     105 + 
     106 + people = (
     107 + <div className={styles.authors}>
     108 + {combinedList?.map((person) => (
     109 + <Person
     110 + key={person}
     111 + name={person}
     112 + image='https://via.placeholder.com/36'
     113 + checked={selectedKeywords.includes(person)}
     114 + onClick={() => selectHandler(person)}
     115 + />
     116 + ))}
     117 + </div>
     118 + );
     119 + } else {
     120 + checkboxes = (
     121 + <div className={styles.checkboxGroup}>
     122 + {simpleCategory?.map((name) => (
     123 + <Checkbox
     124 + key={name}
     125 + label={name}
     126 + checked={selectedKeywords.includes(name)}
     127 + onChange={() => selectHandler(name)}
     128 + />
     129 + ))}
     130 + </div>
     131 + );
     132 + }
     133 + 
     134 + let renderedList;
     135 + 
     136 + switch (renderComponent) {
     137 + case 'chip':
     138 + renderedList = chips;
     139 + break;
     140 + case 'checkbox':
     141 + renderedList = checkboxes;
     142 + break;
     143 + case 'person':
     144 + renderedList = people;
     145 + break;
     146 + }
     147 + 
     148 + return (
     149 + <div className={styles.category}>
     150 + <div className={styles.sidebarItemTop}>
     151 + <div className={styles.sidebarItemTitle}>
     152 + Keywords
     153 + {selectedKeywords.length > 0 && (
     154 + <span className={styles.selectedCounter}>
     155 + <Badge content={selectedKeywords.length} />
     156 + </span>
     157 + )}
     158 + </div>
     159 + {!open && searchable && (
     160 + <div className={styles.sidebarItemAction} onClick={toggleOpen}>
     161 + <Icon kind='search' width={24} height={24} />
     162 + </div>
     163 + )}
     164 + </div>
     165 + 
     166 + {open && searchable ? (
     167 + <SidebarCategorySearch
     168 + searchValue={searchValue}
     169 + selectHandler={selectHandler}
     170 + searchChangeHandler={searchChangeHandler}
     171 + renderComponent={renderComponent}
     172 + clearInput={clearInput}
     173 + selectedItems={selectedKeywords}
     174 + list={list}
     175 + />
     176 + ) : (
     177 + renderedList
     178 + )}
     179 + 
     180 + {searchable && (
     181 + <span role='button' className={styles.toggleView} onClick={toggleOpen}>
     182 + {open ? 'Hide' : 'View All'}
     183 + </span>
     184 + )}
     185 + </div>
     186 + );
     187 +}
     188 + 
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/ui/SidebarCategorySearch/SidebarCategorySearch.module.scss
     1 +@import '~styles/_vars.scss';
     2 + 
     3 +.searchWrapper {
     4 + position: relative;
     5 + margin-bottom: 16px;
     6 +}
     7 + 
     8 +.search {
     9 + display: block;
     10 + width: 100%;
     11 + height: 48px;
     12 + border-radius: 33px;
     13 + background-color: $gray-surface;
     14 + padding: 11px 48px 11px 20px;
     15 + border: none;
     16 + appearance: none;
     17 + outline: none;
     18 + transition: background-color $transition-duration $transition-timing-function;
     19 + 
     20 + &:hover {
     21 + background-color: $gray-border;
     22 + }
     23 +}
     24 + 
     25 +.searchEmpty {
     26 + padding-left: 48px;
     27 +}
     28 + 
     29 +.lookingGlass,
     30 +.clearWrapper {
     31 + position: absolute;
     32 +}
     33 + 
     34 +.lookingGlass {
     35 + top: 12px;
     36 + left: 14px;
     37 +}
     38 + 
     39 +.clearWrapper {
     40 + cursor: pointer;
     41 + top: 12px;
     42 + right: 11px;
     43 + width: 24px;
     44 + height: 24px;
     45 + flex-shrink: 0;
     46 + display: flex;
     47 + align-items: center;
     48 + justify-content: center;
     49 +}
     50 + 
     51 +.groups {
     52 + max-height: 259px;
     53 + overflow-y: auto;
     54 + margin-bottom: 12px;
     55 + padding-right: 16px;
     56 + 
     57 + &::-webkit-scrollbar {
     58 + width: 6px;
     59 + }
     60 + 
     61 + &::-webkit-scrollbar-thumb {
     62 + background: $gray-border;
     63 + border-radius: 14px;
     64 + 
     65 + &:hover {
     66 + background: $gray-text;
     67 + }
     68 + }
     69 +}
     70 + 
     71 +.groupName {
     72 + text-transform: uppercase;
     73 + font-weight: 500;
     74 + color: $gray-text;
     75 + padding-bottom: 6px;
     76 + border-bottom: 1px solid rgba($gray-border, 0.5);
     77 +}
     78 + 
     79 +.groupList {
     80 + padding: 15px 0;
     81 +}
     82 + 
     83 +.groupItem {
     84 + position: relative;
     85 + cursor: pointer;
     86 + padding: 3px 0;
     87 + display: flex;
     88 + align-items: center;
     89 + justify-content: flex-start;
     90 + 
     91 + &:not(:last-child) {
     92 + margin-bottom: 15px;
     93 + }
     94 +}
     95 + 
     96 +.groupItemImage {
     97 + flex-shrink: 0;
     98 + width: 36px;
     99 + height: 36px;
     100 + border-radius: 50%;
     101 + object-fit: cover;
     102 + margin-right: 12px;
     103 +}
     104 + 
     105 +.groupItemActive {
     106 + font-weight: 500;
     107 +}
     108 + 
     109 +.groupItemName {
     110 + flex-grow: 1;
     111 +}
     112 + 
     113 +.groupItemCheck {
     114 + flex-shrink: 0;
     115 + margin-left: 8px;
     116 + margin-right: 8px;
     117 +}
     118 + 
  • ■ ■ ■ ■ ■ ■
    packages/web/src/components/ui/SidebarCategorySearch/SidebarCategorySearch.tsx
     1 +import React from 'react';
     2 +import styles from './SidebarCategorySearch.module.scss';
     3 +import { Icon } from '../Icon/Icon';
     4 +import clsx from 'clsx';
     5 + 
     6 +type listItem = {
     7 + id: string;
     8 + name: string;
     9 +};
     10 + 
     11 +type GroupItem = {
     12 + group: string;
     13 + children: listItem[];
     14 +};
     15 + 
     16 +type Props = {
     17 + searchValue: string;
     18 + searchChangeHandler: (e: React.ChangeEvent<HTMLInputElement>) => void;
     19 + clearInput: () => void;
     20 + list: GroupItem[] | [];
     21 + selectedItems: string[];
     22 + renderComponent: string;
     23 + selectHandler: (name: string) => void;
     24 +};
     25 + 
     26 +export default function SidebarCategorySearch({
     27 + searchValue,
     28 + searchChangeHandler,
     29 + clearInput,
     30 + list,
     31 + selectedItems,
     32 + renderComponent,
     33 + selectHandler,
     34 +}: Props) {
     35 + return (
     36 + <>
     37 + <div className={styles.searchWrapper}>
     38 + {/* FIXME: Don't know why looking glass icon needs to be to the left side and */}
     39 + {/* disappear if input is not empty. Why not show it on right side only like in */}
     40 + {/* header search bar and toggle clear icon when not empty, probably design mistake */}
     41 + {searchValue.length === 0 && (
     42 + <Icon
     43 + kind='search'
     44 + width={24}
     45 + height={24}
     46 + color='#8E8AA0'
     47 + className={styles.lookingGlass}
     48 + />
     49 + )}
     50 + <input
     51 + type='text'
     52 + className={clsx(styles.search, searchValue.length === 0 && styles.searchEmpty)}
     53 + placeholder='Name'
     54 + value={searchValue}
     55 + onChange={searchChangeHandler}
     56 + />
     57 + {searchValue.length > 0 && (
     58 + <span className={styles.clearWrapper}>
     59 + <Icon kind='cross' color='#8E8AA0' className={styles.clear} onClick={clearInput} />
     60 + </span>
     61 + )}
     62 + </div>
     63 + 
     64 + <div className={styles.groups}>
     65 + {list?.length > 0 &&
     66 + list.map(({ group, children }) => (
     67 + <div key={group}>
     68 + {list.length > 1 && <div className={styles.groupName}>{group}</div>}
     69 + <div className={styles.groupList}>
     70 + {children.map(({ name, id }: listItem) => (
     71 + <div
     72 + key={id}
     73 + className={clsx(
     74 + styles.groupItem,
     75 + selectedItems.includes(name) && styles.groupItemActive
     76 + )}
     77 + onClick={() => selectHandler(name)}
     78 + >
     79 + {renderComponent === 'person' && (
     80 + <img
     81 + src='https://via.placeholder.com/36'
     82 + className={styles.groupItemImage}
     83 + alt=''
     84 + />
     85 + )}
     86 + <span className={styles.groupItemName}>{name}</span>
     87 + {selectedItems.includes(name) && (
     88 + <Icon
     89 + kind='check'
     90 + width={12}
     91 + height={10}
     92 + color='#212121'
     93 + className={styles.groupItemCheck}
     94 + />
     95 + )}
     96 + </div>
     97 + ))}
     98 + </div>
     99 + </div>
     100 + ))}
     101 + </div>
     102 + </>
     103 + );
     104 +}
     105 + 
  • ■ ■ ■ ■
    packages/web/src/components/ui/SitesList/SitesList.module.scss
    skipped 9 lines
    10 10   display: flex;
    11 11   flex-wrap: nowrap;
    12 12   align-items: center;
    13  - justify-content: space-between;
     13 + justify-content: flex-start;
    14 14   overflow-x: auto;
    15 15   
    16 16   &::-webkit-scrollbar {
    skipped 47 lines
Please wait...
Page is in error, reload to recover