Skip to content

Commit a1aeb9a

Browse files
committed
Add pagination to Recent snippets page
To let user navigate through existing snippets and fetch snippets proportinaly we have added two buttons Next and Previous so we can get go throught all snippets by step equal 20
1 parent 014ed76 commit a1aeb9a

File tree

7 files changed

+235
-13
lines changed

7 files changed

+235
-13
lines changed

package-lock.json

Lines changed: 9 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"dependencies": {
3030
"codemirror": "^5.33.0",
3131
"immutable": "^3.8.2",
32+
"parse-link-header": "^1.0.1",
3233
"prop-types": "^15.6.0",
3334
"react": "^16.0.0",
3435
"react-codemirror2": "^3.0.7",

src/actions/index.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
1+
import parseLinkHeader from 'parse-link-header';
2+
13
export const setRecentSnippets = snippets => ({
24
type: 'SET_RECENT_SNIPPETS',
35
snippets,
46
});
57

6-
export const fetchRecentSnippets = dispatch => (
7-
fetch('http://api.xsnippet.org/snippets')
8-
.then(response => response.json())
9-
.then(json => dispatch(setRecentSnippets(json)))
10-
);
8+
export const setPaginationLinks = links => ({
9+
type: 'SET_PAGINATION_LINKS',
10+
links,
11+
});
12+
13+
export const fetchRecentSnippets = marker => (dispatch) => {
14+
let qs = '';
15+
if (marker) { qs = `&marker=${marker}`; }
16+
17+
return fetch(`http://api.xsnippet.org/snippets?limit=20${qs}`)
18+
.then((response) => {
19+
const links = parseLinkHeader(response.headers.get('Link'));
20+
21+
dispatch(setPaginationLinks(links));
22+
return response.json();
23+
})
24+
.then(json => dispatch(setRecentSnippets(json)));
25+
};
1126

1227
export const setSnippet = snippet => ({
1328
type: 'SET_SNIPPET',

src/components/RecentSnippets.jsx

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,73 @@ import * as actions from '../actions';
88
import '../styles/RecentSnippets.styl';
99

1010
class RecentSnippets extends React.Component {
11+
constructor(props) {
12+
super(props);
13+
this.newerSetOfSnippets = this.newerSetOfSnippets.bind(this);
14+
this.olderSetOfSnippets = this.olderSetOfSnippets.bind(this);
15+
}
16+
1117
componentDidMount() {
12-
const { dispatch } = this.props;
13-
dispatch(actions.fetchRecentSnippets);
18+
const { dispatch, recent, pagination } = this.props;
19+
let marker = null;
20+
21+
if (pagination.get('prev')) {
22+
marker = recent.get(0) + 1;
23+
}
24+
25+
dispatch(actions.fetchRecentSnippets(marker));
26+
}
27+
28+
newerSetOfSnippets() {
29+
const { dispatch, pagination } = this.props;
30+
const prev = pagination.get('prev');
31+
32+
if (prev) {
33+
const marker = Number(prev.marker);
34+
35+
dispatch(actions.fetchRecentSnippets(marker));
36+
}
37+
}
38+
39+
olderSetOfSnippets() {
40+
const { dispatch, pagination } = this.props;
41+
const marker = Number(pagination.get('next').marker);
42+
43+
dispatch(actions.fetchRecentSnippets(marker));
1444
}
1545

1646
render() {
17-
const { snippets, recent } = this.props;
47+
const { snippets, recent, pagination } = this.props;
48+
const older = pagination.get('next');
49+
const newer = pagination.get('prev');
1850

1951
return ([
2052
<Title title="Recent snippets" additionalClass="recent-title" key="title-recent" />,
2153
<ul className="recent-snippet" key="recent-snippet">
2254
{recent.map(id => <RecentSnippetItem key={id} snippet={snippets.get(id)} />)}
2355
</ul>,
56+
<div className="pagination" key="pagination">
57+
<span
58+
className={`pagination-item next ${newer ? '' : 'disabled'}`}
59+
onClick={this.newerSetOfSnippets}
60+
role="presentation"
61+
>
62+
&lsaquo; Newer
63+
</span>
64+
<span
65+
className={`pagination-item prev ${older ? '' : 'disabled'}`}
66+
onClick={this.olderSetOfSnippets}
67+
role="presentation"
68+
>
69+
Older &rsaquo;
70+
</span>
71+
</div>,
2472
]);
2573
}
2674
}
2775

2876
export default connect(state => ({
2977
snippets: state.get('snippets'),
3078
recent: state.get('recent'),
79+
pagination: state.get('pagination'),
3180
}))(RecentSnippets);

src/reducers/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ const recent = (state = List(), action) => {
2424
}
2525
};
2626

27+
const pagination = (state = Map(), action) => {
28+
switch (action.type) {
29+
case 'SET_PAGINATION_LINKS':
30+
return Map(action.links);
31+
32+
default:
33+
return state;
34+
}
35+
};
36+
2737
const syntaxes = (state = List(), action) => {
2838
switch (action.type) {
2939
case 'SET_SYNTAXES':
@@ -38,4 +48,5 @@ export default combineReducers({
3848
snippets,
3949
recent,
4050
syntaxes,
51+
pagination,
4152
});

src/styles/RecentSnippets.styl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,23 @@
5252
background-color: button-light-normal
5353
&:hover
5454
background-color: button-light-active
55+
56+
.pagination
57+
display: flex
58+
flex-flow: row nowrap
59+
justify-content: flex-end
60+
padding-top: 10px
61+
&-item
62+
padding: 10px 14px
63+
font-size: 17px
64+
font-family: font-quicksand
65+
background-color: button-normal
66+
color: text-light
67+
cursor: pointer
68+
&:hover
69+
background-color: button-active
70+
&.next
71+
margin-right: 4px
72+
&.disabled
73+
pointer-events: none
74+
background-color: button-light-normal

tests/store.test.js

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,104 @@ describe('actions', () => {
3535
},
3636
},
3737
syntaxes: [],
38+
pagination: {},
3839
});
3940
});
4041

41-
it('should create an action to fetch recent snippets', async () => {
42+
it('should create an action to set pagination links', () => {
43+
const links = {
44+
first: {
45+
limit: '20',
46+
rel: 'first',
47+
url: 'http://api.xsnippet.org/snippets?limit=20',
48+
},
49+
next: {
50+
limit: '20',
51+
marker: 28,
52+
rel: 'next',
53+
url: 'http://api.xsnippet.org/snippets?limit=20&marker=28',
54+
},
55+
prev: {
56+
limit: '20',
57+
rel: 'prev',
58+
url: 'http://api.xsnippet.org/snippets?limit=20',
59+
},
60+
};
61+
const store = createStore();
62+
store.dispatch(actions.setPaginationLinks(links));
63+
64+
expect(store.getState().toJS()).toEqual({
65+
recent: [],
66+
snippets: {},
67+
syntaxes: [],
68+
pagination: links,
69+
});
70+
});
71+
72+
it('should create an action to fetch recent snippets with marker', async () => {
73+
const snippets = [
74+
{
75+
id: 1,
76+
content: 'test',
77+
syntax: 'JavaScript',
78+
},
79+
{
80+
id: 2,
81+
content: 'batman',
82+
syntax: 'Python',
83+
},
84+
];
85+
const links = '<http://api.xsnippet.org/snippets?limit=20>; rel="first", <http://api.xsnippet.org/snippets?limit=20&marker=19>; rel="next", <http://api.xsnippet.org/snippets?limit=20&marker=59>; rel="prev"';
86+
87+
fetchMock.getOnce(
88+
'http://api.xsnippet.org/snippets?limit=20&marker=39',
89+
{
90+
headers: { Link: links },
91+
body: snippets,
92+
},
93+
);
94+
95+
const store = createStore();
96+
await store.dispatch(actions.fetchRecentSnippets(39));
97+
98+
expect(store.getState().toJS()).toEqual({
99+
recent: [1, 2],
100+
snippets: {
101+
1: {
102+
id: 1,
103+
content: 'test',
104+
syntax: 'JavaScript',
105+
},
106+
2: {
107+
id: 2,
108+
content: 'batman',
109+
syntax: 'Python',
110+
},
111+
},
112+
syntaxes: [],
113+
pagination: {
114+
first: {
115+
limit: '20',
116+
rel: 'first',
117+
url: 'http://api.xsnippet.org/snippets?limit=20',
118+
},
119+
next: {
120+
limit: '20',
121+
marker: '19',
122+
rel: 'next',
123+
url: 'http://api.xsnippet.org/snippets?limit=20&marker=19',
124+
},
125+
prev: {
126+
limit: '20',
127+
marker: '59',
128+
rel: 'prev',
129+
url: 'http://api.xsnippet.org/snippets?limit=20&marker=59',
130+
},
131+
},
132+
});
133+
});
134+
135+
it('should create an action to fetch recent snippets without marker', async () => {
42136
const snippets = [
43137
{
44138
id: 1,
@@ -51,11 +145,18 @@ describe('actions', () => {
51145
syntax: 'Python',
52146
},
53147
];
148+
const links = '<http://api.xsnippet.org/snippets?limit=20>; rel="first", <http://api.xsnippet.org/snippets?limit=20&marker=39>; rel="next"';
54149

55-
fetchMock.getOnce('http://api.xsnippet.org/snippets', JSON.stringify(snippets));
150+
fetchMock.getOnce(
151+
'http://api.xsnippet.org/snippets?limit=20',
152+
{
153+
headers: { Link: links },
154+
body: snippets,
155+
},
156+
);
56157

57158
const store = createStore();
58-
await store.dispatch(actions.fetchRecentSnippets);
159+
await store.dispatch(actions.fetchRecentSnippets());
59160

60161
expect(store.getState().toJS()).toEqual({
61162
recent: [1, 2],
@@ -72,6 +173,19 @@ describe('actions', () => {
72173
},
73174
},
74175
syntaxes: [],
176+
pagination: {
177+
first: {
178+
limit: '20',
179+
rel: 'first',
180+
url: 'http://api.xsnippet.org/snippets?limit=20',
181+
},
182+
next: {
183+
limit: '20',
184+
marker: '39',
185+
rel: 'next',
186+
url: 'http://api.xsnippet.org/snippets?limit=20&marker=39',
187+
},
188+
},
75189
});
76190
});
77191

@@ -94,6 +208,7 @@ describe('actions', () => {
94208
},
95209
},
96210
syntaxes: [],
211+
pagination: {},
97212
});
98213
});
99214

@@ -119,6 +234,7 @@ describe('actions', () => {
119234
},
120235
},
121236
syntaxes: [],
237+
pagination: {},
122238
});
123239
});
124240

@@ -130,6 +246,7 @@ describe('actions', () => {
130246
expect(store.getState().toJS()).toEqual({
131247
recent: [],
132248
snippets: {},
249+
pagination: {},
133250
syntaxes,
134251
});
135252
});
@@ -145,6 +262,7 @@ describe('actions', () => {
145262
expect(store.getState().toJS()).toEqual({
146263
recent: [],
147264
snippets: {},
265+
pagination: {},
148266
syntaxes,
149267
});
150268
});
@@ -171,6 +289,7 @@ describe('actions', () => {
171289
},
172290
},
173291
syntaxes: [],
292+
pagination: {},
174293
});
175294
});
176295
});

0 commit comments

Comments
 (0)