Skip to content

Commit 22a2ce1

Browse files
authored
Merge pull request #49 from xsnippet/pagination
Add pagination to Recent snippets page
2 parents 3844251 + a1aeb9a commit 22a2ce1

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)