Skip to content

Commit b37d62e

Browse files
committed
implement the card filter on the mobile version
1 parent 6363cf7 commit b37d62e

23 files changed

Lines changed: 464 additions & 254 deletions

File tree

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"axios-cache-adapter": "^2.7.3",
1515
"country-emoji": "^1.5.4",
1616
"dompurify": "^2.2.7",
17+
"eslint-plugin-react": "^7.28.0",
1718
"localforage": "^1.9.0",
1819
"normalize.css": "^8.0.1",
1920
"prop-types": "^15.0.0-0",
@@ -29,14 +30,14 @@
2930
"react-scripts": "4.0.1",
3031
"react-select": "^5.0.1",
3132
"react-spinners": "^0.10.4",
33+
"react-spring-bottom-sheet": "^3.4.1",
3234
"react-toggle": "^4.1.1",
3335
"react-tooltip": "^4.2.21",
3436
"styled-components": "2",
3537
"timeago.js": "^4.0.2",
3638
"type-fest": "^1.2.0",
3739
"web-vitals": "^0.2.4",
38-
"zustand": "^4.1.3",
39-
"eslint-plugin-react": "^7.28.0"
40+
"zustand": "^4.1.3"
4041
},
4142
"proxy": "https://api.hackertab.dev/",
4243
"scripts": {

src/assets/App.css

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,34 @@ Producthunt item
963963
margin-right: 16px;
964964
border-radius: 4px;
965965
}
966+
967+
.floatingFilter {
968+
background: rgb(44, 128, 232);
969+
border: transparent;
970+
width: 48px;
971+
height: 48px;
972+
border-radius: 48px;
973+
position: fixed;
974+
bottom: 78px;
975+
z-index: 2;
976+
right: 32px;
977+
display: none;
978+
box-shadow: 0 2px 8px var(--card-actions-background-shadow);
979+
}
980+
.floatingFilterBottomSheet .title {
981+
font-size: 22px;
982+
margin:0 0 8px 0;
983+
}
984+
985+
.floatingFilterIcon {
986+
color: white;
987+
font-size: 24px;
988+
}
989+
@media only screen and (max-width: 600px) {
990+
.floatingFilter {
991+
display: block
992+
}
993+
}
966994
/* Small devices (portrait tablets and large phones, 600px and up) */
967995
@media only screen and (min-width: 600px) {
968996
.block {
@@ -1038,4 +1066,26 @@ Producthunt item
10381066
.Page .buttonIcon {
10391067
position: relative;
10401068
vertical-align: middle;
1069+
}
1070+
1071+
1072+
.chipsSet {
1073+
margin: 6px 0;
1074+
display: flex;
1075+
gap: 12px;
1076+
flex-wrap: wrap;
1077+
}
1078+
1079+
.chip {
1080+
font-weight: 500;
1081+
border-radius: 50px;
1082+
padding:6px 12px;
1083+
border: transparent;
1084+
font-family: "nunito";
1085+
color:var(--chip-text);
1086+
background-color: var(--chip-background);
1087+
}
1088+
.chip.active {
1089+
background-color: var(--chip-active-background);
1090+
color:var(--chip-active-text);
10411091
}

src/assets/variables.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ html.dark {
5959
--tooltip-accent-color: #0366D6;
6060

6161
--placeholder-background-color: #1c2026;
62+
63+
--rsbs-bg: #0D1116;
64+
--chip-background: #0D1116;
65+
--chip-text: #FFF;
66+
--chip-active-background: #576172;
67+
--chip-active-text: #FFF;
6268
}
6369

6470
html.light {
@@ -110,4 +116,11 @@ html.light {
110116

111117
--placeholder-background-color: #e7eff7;
112118

119+
--rsbs-bg: #fff;
120+
121+
--chip-background: #e7eff7;
122+
--chip-text: #3C5065;
123+
--chip-active-background: #0366D6;
124+
--chip-active-text: #FFF;
125+
113126
}

src/components/Elements/Card/Card.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,10 @@ type CardProps = {
1010
titleComponent?: React.ReactNode
1111
fullBlock?: boolean
1212
}
13-
export const Card = ({
14-
card: { link, icon, label },
15-
titleComponent,
16-
children,
17-
fullBlock = false,
18-
}: CardProps) => {
19-
const { openLinksNewTab } = useUserPreferences()
2013

14+
export const Card = ({ card, titleComponent, children, fullBlock = false }: CardProps) => {
15+
const { openLinksNewTab } = useUserPreferences()
16+
const { link, icon, label } = card
2117
const handleHeaderLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
2218
e.preventDefault()
2319
let url = `${link}?${ref}`
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Option } from 'src/types'
2+
import { useState } from 'react'
3+
4+
type ChipProps = {
5+
option: Option
6+
onSelect: (option: Option) => void
7+
active: boolean
8+
}
9+
10+
const Chip = ({ option, onSelect, active = false }: ChipProps) => {
11+
return (
12+
<button className={'chip ' + (active && 'active')} onClick={() => onSelect(option)}>
13+
{option.label}
14+
</button>
15+
)
16+
}
17+
18+
type ChipsSetProps = {
19+
options: Option[]
20+
defaultValue: Option
21+
onChange: (option: Option) => void
22+
}
23+
24+
export const ChipsSet = ({ options, onChange, defaultValue }: ChipsSetProps) => {
25+
const [selectedChip, setSelectedChip] = useState<Option>(defaultValue)
26+
27+
const onSelect = (option: Option) => {
28+
setSelectedChip(option)
29+
onChange(option)
30+
}
31+
32+
return (
33+
<div className="chipsSet">
34+
{options.map((option) => {
35+
return (
36+
<Chip option={option} onSelect={onSelect} active={selectedChip.value === option.value} />
37+
)
38+
})}
39+
</div>
40+
)
41+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./ChipsSet"
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { SupportedCard, GLOBAL_TAG, MY_LANGUAGES_TAG, dateRanges } from 'src/config'
2+
import { FiFilter } from 'react-icons/fi'
3+
import { useState } from 'react'
4+
import { BottomSheet } from 'react-spring-bottom-sheet'
5+
import 'react-spring-bottom-sheet/dist/style.css'
6+
import { useUserPreferences } from 'src/stores/preferences'
7+
import { trackCardLanguageSelect, trackCardDateRangeSelect } from 'src/lib/analytics'
8+
import { ChipsSet } from 'src/components/Elements'
9+
10+
type ListingFilterMobileProps = {
11+
card: SupportedCard
12+
filters?: ('datesRange' | 'language')[]
13+
}
14+
15+
export const FloatingFilter = ({ card, filters = ['language'] }: ListingFilterMobileProps) => {
16+
const [open, setOpen] = useState(false)
17+
const { userSelectedTags, cardsSettings, setCardSettings } = useUserPreferences()
18+
const [availableTagOptions] = useState(
19+
[GLOBAL_TAG, ...userSelectedTags, MY_LANGUAGES_TAG].map((tag) => ({
20+
label: tag.label,
21+
value: tag.value,
22+
}))
23+
)
24+
return (
25+
<>
26+
<button
27+
className="floatingFilter"
28+
onClick={() => setOpen(true)}
29+
style={open ? { display: 'none' } : {}}>
30+
<FiFilter className="floatingFilterIcon" />
31+
</button>
32+
33+
<BottomSheet
34+
defaultSnap={({ maxHeight }) => maxHeight / 2}
35+
open={open}
36+
expandOnContentDrag={true}
37+
onDismiss={() => setOpen(false)}>
38+
<div style={{ padding: '16px' }}>
39+
<div className="settings floatingFilterBottomSheet">
40+
<h1 className="title">Customize {card.label}</h1>
41+
42+
{filters.includes('language') && (
43+
<div className="settingRow">
44+
<p className="settingTitle">Language</p>
45+
<div className="settingContent">
46+
<ChipsSet
47+
defaultValue={
48+
availableTagOptions.find(
49+
(tag) => tag.value === cardsSettings[card.value]?.language
50+
) || {
51+
label: GLOBAL_TAG.label,
52+
value: GLOBAL_TAG.value,
53+
}
54+
}
55+
options={availableTagOptions}
56+
onChange={(option) => {
57+
setCardSettings(card.value, {
58+
...cardsSettings[card.value],
59+
language: option.value,
60+
})
61+
trackCardLanguageSelect(card.analyticsTag, option.value)
62+
}}
63+
/>
64+
</div>
65+
</div>
66+
)}
67+
68+
{filters.includes('datesRange') && (
69+
<div className="settingRow">
70+
<p className="settingTitle">Date Range</p>
71+
<div className="settingContent">
72+
<ChipsSet
73+
defaultValue={
74+
dateRanges.find(
75+
(date) => date.value === cardsSettings[card.value]?.dateRange
76+
) || dateRanges[0]
77+
}
78+
options={dateRanges}
79+
onChange={(option) => {
80+
setCardSettings(card.value, {
81+
...cardsSettings[card.value],
82+
dateRange: option.value,
83+
})
84+
trackCardDateRangeSelect(card.analyticsTag, option.value)
85+
}}
86+
/>
87+
</div>
88+
</div>
89+
)}
90+
</div>
91+
</div>
92+
</BottomSheet>
93+
</>
94+
)
95+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./FloatingFilter"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import DropDownMenu from '../../DropDownMenu'
2+
3+
type Option = {
4+
label: string
5+
value: string
6+
}
7+
type InlineTextFilterProps = {
8+
options: Option[]
9+
value: string | undefined
10+
onChange: (option: Option) => void
11+
}
12+
13+
export const InlineTextFilter = ({ options, value, onChange }: InlineTextFilterProps) => {
14+
const tagId = `inline-text-filter-${Math.random().toString(16).slice(2)}`
15+
return (
16+
<DropDownMenu
17+
data={options}
18+
tagId={tagId}
19+
label={options.find((opt) => opt.value === value)?.label || options[0].label}
20+
setSelectedDropDownItem={(item: Option) => {
21+
onChange(item)
22+
}}
23+
/>
24+
)
25+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./InlineTextFilter"

0 commit comments

Comments
 (0)