Skip to content

Commit 2e65a7e

Browse files
committed
Allow users to choose trending timeframe
1 parent cdb6773 commit 2e65a7e

File tree

7 files changed

+252
-19
lines changed

7 files changed

+252
-19
lines changed

src/components/home/HomeTab.tsx

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@ import React, { useRef } from 'react';
22
import styled from 'styled-components';
33
import { NavLink, useLocation } from 'react-router-dom';
44
import palette from '../../lib/styles/palette';
5-
import { MdTrendingUp, MdAccessTime, MdMoreVert } from 'react-icons/md';
5+
import {
6+
MdTrendingUp,
7+
MdAccessTime,
8+
MdMoreVert,
9+
MdArrowDropDown,
10+
} from 'react-icons/md';
611
import { useSpring, animated } from 'react-spring';
712
import { mediaQuery } from '../../lib/styles/media';
813
import useToggle from '../../lib/hooks/useToggle';
914
import HomeMobileHeadExtra from './HomeMobileHeadExtra';
15+
import TimeframePicker from './TimeframePicker';
16+
import { useTimeframe } from './hooks/useTimeframe';
17+
import { timeframes } from './utils/timeframeMap';
1018

1119
export type HomeTabProps = {};
1220

@@ -15,7 +23,10 @@ function HomeTab(props: HomeTabProps) {
1523

1624
const isRecent = location.pathname === '/recent';
1725
const [extra, toggle] = useToggle(false);
26+
const [timeframePicker, toggleTimeframePicker] = useToggle(false);
1827
const moreButtonRef = useRef<HTMLDivElement | null>(null);
28+
const timeframeRef = useRef<HTMLDivElement | null>(null);
29+
const [timeframe] = useTimeframe();
1930

2031
const onClose = (e: React.MouseEvent<HTMLElement>) => {
2132
if (!moreButtonRef.current) return;
@@ -28,6 +39,17 @@ function HomeTab(props: HomeTabProps) {
2839
toggle();
2940
};
3041

42+
const onCloseTimeframePicker = (e: React.MouseEvent<HTMLElement>) => {
43+
if (!timeframeRef.current) return;
44+
if (
45+
e.target === timeframeRef.current ||
46+
timeframeRef.current.contains(e.target as Node)
47+
) {
48+
return;
49+
}
50+
toggleTimeframePicker();
51+
};
52+
3153
const springStyle = useSpring({
3254
left: isRecent ? '50%' : '0%',
3355
config: {
@@ -38,23 +60,37 @@ function HomeTab(props: HomeTabProps) {
3860

3961
return (
4062
<Wrapper>
41-
<Block>
42-
<NavLink
43-
to="/"
44-
activeClassName="active"
45-
isActive={(match, location) => {
46-
return ['/', '/trending'].indexOf(location.pathname) !== -1;
47-
}}
48-
>
49-
<MdTrendingUp />
50-
트렌딩
51-
</NavLink>
52-
<NavLink to="/recent" activeClassName="active">
53-
<MdAccessTime />
54-
최신
55-
</NavLink>
56-
<Indicator style={springStyle} />
57-
</Block>
63+
<Left>
64+
<Block>
65+
<NavLink
66+
to="/"
67+
activeClassName="active"
68+
isActive={(match, location) => {
69+
return ['/', '/trending'].indexOf(location.pathname) !== -1;
70+
}}
71+
>
72+
<MdTrendingUp />
73+
트렌딩
74+
</NavLink>
75+
<NavLink to="/recent" activeClassName="active">
76+
<MdAccessTime />
77+
최신
78+
</NavLink>
79+
<Indicator style={springStyle} />
80+
</Block>
81+
{['/', '/trending'].includes(location.pathname) && (
82+
<>
83+
<Selector onClick={toggleTimeframePicker} ref={timeframeRef}>
84+
{timeframes.find((t) => t[0] === timeframe)![1]}{' '}
85+
<MdArrowDropDown />
86+
</Selector>
87+
<TimeframePicker
88+
visible={timeframePicker}
89+
onClose={onCloseTimeframePicker}
90+
/>
91+
</>
92+
)}
93+
</Left>
5894
<MobileMore ref={moreButtonRef}>
5995
<MdMoreVert className="more" onClick={toggle} />
6096
</MobileMore>
@@ -82,6 +118,12 @@ const MobileMore = styled.div`
82118
justify-content: center;
83119
`;
84120

121+
const Left = styled.div`
122+
display: flex;
123+
align-items: center;
124+
position: relative;
125+
`;
126+
85127
const Block = styled.div`
86128
display: flex;
87129
position: relative;
@@ -126,4 +168,30 @@ const Indicator = styled(animated.div)`
126168
background: ${palette.gray8};
127169
`;
128170

171+
const Selector = styled.div`
172+
background: white;
173+
height: 2rem;
174+
width: 5rem;
175+
border-radius: 4px;
176+
display: flex;
177+
align-items: center;
178+
justify-content: space-between;
179+
padding-left: 0.5rem;
180+
padding-right: 0.5rem;
181+
font-weight: 600;
182+
color: ${palette.gray7};
183+
font-size: 0.875rem;
184+
box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.05);
185+
svg {
186+
width: 1.5rem;
187+
height: 1.5rem;
188+
}
189+
cursor: pointer;
190+
@media (hover: hover) and (pointer: fine) {
191+
&:hover {
192+
opacity: 0.75;
193+
}
194+
}
195+
`;
196+
129197
export default HomeTab;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React from 'react';
2+
import styled from 'styled-components';
3+
import palette from '../../lib/styles/palette';
4+
import { useTransition, animated } from 'react-spring';
5+
import OutsideClickHandler from 'react-outside-click-handler';
6+
import { useTimeframe } from './hooks/useTimeframe';
7+
import { timeframes } from './utils/timeframeMap';
8+
9+
export type TimeframePickerProps = {
10+
visible: boolean;
11+
onClose: (e: React.MouseEvent<HTMLElement>) => void;
12+
};
13+
14+
function TimeframePicker({ visible, onClose }: TimeframePickerProps) {
15+
const transition = useTransition(visible, null, {
16+
from: {
17+
opacity: 0,
18+
transform: 'scale(0.8)',
19+
},
20+
enter: {
21+
opacity: 1,
22+
transform: 'scale(1)',
23+
},
24+
leave: {
25+
opacity: 0,
26+
transform: 'scale(0.8)',
27+
},
28+
config: {
29+
tension: 350,
30+
friction: 26,
31+
},
32+
});
33+
34+
const [timeframe, setTimeframe] = useTimeframe();
35+
36+
return (
37+
<>
38+
{transition.map(({ item, key, props }) =>
39+
item ? (
40+
<Aligner key={key}>
41+
<OutsideClickHandler onOutsideClick={onClose} key={key}>
42+
<Block style={props} onClick={onClose}>
43+
<ul>
44+
{timeframes.map(([value, text]) => (
45+
<li
46+
key={value}
47+
onClick={() => setTimeframe(value)}
48+
className={value === timeframe ? 'active' : ''}
49+
>
50+
{text}
51+
</li>
52+
))}
53+
</ul>
54+
</Block>
55+
</OutsideClickHandler>
56+
</Aligner>
57+
) : null,
58+
)}
59+
</>
60+
);
61+
}
62+
63+
const Aligner = styled.div`
64+
position: absolute;
65+
right: 0;
66+
top: 100%;
67+
z-index: 5;
68+
`;
69+
const Block = styled(animated.div)`
70+
margin-top: 0.5rem;
71+
width: 12rem;
72+
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
73+
background: white;
74+
color: ${palette.gray9};
75+
transform-origin: top right;
76+
ul {
77+
list-style: none;
78+
padding-left: 0;
79+
margin: 0;
80+
}
81+
li {
82+
cursor: pointer;
83+
&:hover {
84+
background: ${palette.gray0};
85+
}
86+
font-weight: 600;
87+
88+
font-size: 0.875rem;
89+
padding: 0.75rem 1rem;
90+
91+
&.active {
92+
color: ${palette.teal6};
93+
}
94+
}
95+
li + li {
96+
border-top: 1px solid ${palette.gray1};
97+
}
98+
.contact {
99+
border-top: 1px solid #f1f3f5;
100+
padding: 1rem;
101+
h5 {
102+
margin: 0;
103+
font-size: 0.75rem;
104+
}
105+
.email {
106+
color: ${palette.gray8};
107+
font-size: 0.75rem;
108+
}
109+
}
110+
`;
111+
112+
export default TimeframePicker;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useDispatch, useSelector } from 'react-redux';
2+
import { useMemo } from 'react';
3+
import { bindActionCreators } from 'redux';
4+
import home from '../../../modules/home';
5+
import { RootState } from '../../../modules';
6+
7+
export function useTimeframe() {
8+
const dispatch = useDispatch();
9+
const actions = useMemo(
10+
() => bindActionCreators(home.actions, dispatch),
11+
[dispatch],
12+
);
13+
const timeframe = useSelector((state: RootState) => state.home.timeframe);
14+
15+
return [timeframe, actions.choose] as const;
16+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Timeframe } from '../../../modules/home';
2+
3+
export const timeframeMap: Record<Timeframe, string> = {
4+
day: '오늘',
5+
week: '이번 주',
6+
month: '이번 달',
7+
year: '올해',
8+
};
9+
10+
export const timeframes = Object.entries(timeframeMap) as [Timeframe, string][];

src/modules/home.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2+
3+
export type Timeframe = 'day' | 'week' | 'month' | 'year';
4+
export interface HomeState {
5+
timeframe: Timeframe;
6+
}
7+
8+
const initialState: HomeState = {
9+
timeframe: 'week',
10+
};
11+
const home = createSlice({
12+
name: 'home',
13+
initialState,
14+
reducers: {
15+
choose(state, action: PayloadAction<Timeframe>) {
16+
state.timeframe = action.payload;
17+
},
18+
},
19+
});
20+
21+
export default home;

src/modules/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import header, { HeaderState } from './header';
55
import post, { PostState } from './post';
66
import error, { ErrorState } from './error';
77
import scroll, { ScrollState } from './scroll';
8+
import home, { HomeState } from './home';
89

910
export type RootState = {
1011
core: CoreState;
@@ -13,6 +14,7 @@ export type RootState = {
1314
post: PostState;
1415
error: ErrorState;
1516
scroll: ScrollState;
17+
home: HomeState;
1618
};
1719

1820
const rootReducer = combineReducers({
@@ -22,6 +24,7 @@ const rootReducer = combineReducers({
2224
post: post.reducer,
2325
error: error.reducer,
2426
scroll: scroll.reducer,
27+
home: home.reducer,
2528
});
2629

2730
export default rootReducer;

src/pages/home/hooks/useTrendingPosts.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import {
55
import { useQuery } from '@apollo/react-hooks';
66
import { useCallback, useState } from 'react';
77
import useScrollPagination from '../../../lib/hooks/useScrollPagination';
8+
import { useTimeframe } from '../../../components/home/hooks/useTimeframe';
89

910
export default function useTrendingPosts() {
11+
const [timeframe] = useTimeframe();
1012
const { data, loading, fetchMore } = useQuery<GetTrendingPostsResponse>(
1113
GET_TRENDING_POSTS,
1214
{
1315
variables: {
1416
limit: 24,
17+
timeframe: timeframe,
1518
},
1619
// https://github.com/apollographql/apollo-client/issues/1617
1720
notifyOnNetworkStatusChange: true,
@@ -42,7 +45,7 @@ export default function useTrendingPosts() {
4245
);
4346

4447
const uniquePosts = fetchMoreResult.trendingPosts.filter(
45-
post => !idMap[post.id],
48+
(post) => !idMap[post.id],
4649
);
4750

4851
return {

0 commit comments

Comments
 (0)