diff --git a/client/app/assets/arrow-left.gif b/client/app/assets/arrow-left.gif new file mode 100644 index 0000000..c30dfbb Binary files /dev/null and b/client/app/assets/arrow-left.gif differ diff --git a/client/app/assets/arrow-right.gif b/client/app/assets/arrow-right.gif new file mode 100644 index 0000000..392b897 Binary files /dev/null and b/client/app/assets/arrow-right.gif differ diff --git a/client/app/components/pagination-links/pagination-links.scss b/client/app/components/pagination-links/pagination-links.scss new file mode 100644 index 0000000..c7d676d --- /dev/null +++ b/client/app/components/pagination-links/pagination-links.scss @@ -0,0 +1,4 @@ +.pagination-links { + display: flex; + align-items: center; +} diff --git a/client/app/components/pagination-links/pagination-links.tsx b/client/app/components/pagination-links/pagination-links.tsx index 067d637..df92cee 100644 --- a/client/app/components/pagination-links/pagination-links.tsx +++ b/client/app/components/pagination-links/pagination-links.tsx @@ -1,9 +1,16 @@ import React from 'react'; +import './pagination-links.scss'; + +const arrows = { + right: require('../../assets/arrow-right.gif'), + left: require('../../assets/arrow-left.gif'), +}; interface Props { pageLinks: (number | string)[]; activePage: number; onPageSelect: (page: number) => void; + showArrows?: boolean; } interface State { @@ -26,11 +33,26 @@ export class PaginationLinks extends React.Component { this.onPageSelect(page)}>{page}; } + onArrowClick(arrow: 'right' | 'left') { + const nextPage = this.props.activePage + (arrow === 'right' ? 1 : -1); + if (this.props.pageLinks.includes(nextPage)) { + this.onPageSelect(nextPage); + } + } + + renderArrow(arrow: 'right' | 'left') { + if (this.props.showArrows) { + return this.onArrowClick(arrow)} className="clickable"/>; + } + return null; + } + render() { const { activePage, pageLinks } = this.props; return ( - + + {this.renderArrow('left')} {pageLinks.map((link, index) => { const active = link === activePage; return ( @@ -43,6 +65,7 @@ export class PaginationLinks extends React.Component { ); })} + {this.renderArrow('right')} ); } diff --git a/client/app/model/reply.ts b/client/app/model/reply.ts index d6fefd7..cc3cd85 100644 --- a/client/app/model/reply.ts +++ b/client/app/model/reply.ts @@ -4,6 +4,7 @@ export interface ReplyModel { content: string; edited: boolean; id: number; + index?: number; inserted_at: string; quote_id: number; thread_id: number; diff --git a/client/app/pages/forum/forum.tsx b/client/app/pages/forum/forum.tsx index e035d7c..7e2a499 100644 --- a/client/app/pages/forum/forum.tsx +++ b/client/app/pages/forum/forum.tsx @@ -99,8 +99,8 @@ export class Forum extends React.Component { this.setState({ threads, pageThreads: [...threads].splice(threadIndex, threadsPerPage), - pageLinks: pagination(page, numPages) }, - ); + pageLinks: pagination(page, numPages), + }); } private orderBy(threads: ThreadModel[], props: Props) { @@ -235,7 +235,7 @@ export class Forum extends React.Component { return (
-
+
Page: { userStore: UserStore; + page: number; } interface State { @@ -20,6 +21,8 @@ interface State { quotedReply?: ReplyModel; showEditor: boolean; thread?: ThreadModel; + replies: ReplyModel[]; + pageLinks: (number | string)[]; } @inject('userStore') @@ -30,6 +33,8 @@ export class Thread extends React.Component { super(props); this.state = { showEditor: false, + replies: [], + pageLinks: [], }; } @@ -37,10 +42,45 @@ export class Thread extends React.Component { this.getReplies(); } + componentWillReceiveProps(nextProps: Props) { + if (this.props.match.params['page'] !== nextProps.match.params['page']) { + this.processReplies(this.state.thread!, nextProps); + } + } + + private routeParams(props: Props = this.props) { + return { + categoryId: parseInt(props.match.params['categoryId'], 10), + threadId: parseInt(props.match.params['threadId'], 10), + page: parseInt(props.match.params['page'], 10) || 1, + }; + } + + private navigateHere(page: number) { + const { categoryId, threadId } = this.routeParams(); + const url = `/t/${categoryId}/${threadId}/${page}`; + this.props.history.push(url); + } + private async getReplies() { const thread = await ThreadService.getThread(this.props.match.params['threadId']); - thread.replies = orderBy(thread.replies, ['inserted_at']); - this.setState({ thread }); + this.processReplies(thread); + } + + private processReplies(thread: ThreadModel, props: Props = this.props) { + thread.replies = chain(thread.replies) + .orderBy(['inserted_at'], ['asc']) + .map((t, i) => { t.index = i; return t; }) + .value(); + const { page } = this.routeParams(props); + const numPages = Math.ceil(thread.replies.length / 20); + const replyIndex = (page - 1) * 20; + + this.setState({ + thread, + replies: [...thread.replies].splice(replyIndex, 20), + pageLinks: pagination(page, numPages), + }); } private onReplyClick() { @@ -122,7 +162,7 @@ export class Thread extends React.Component { } renderReplies(): any { - return this.state.thread!.replies.map((reply, index) => { + return this.state.replies.map((reply, index) => { const replyDark = index % 2 === 0 ? 'reply--dark' : ''; const bluePost = reply.user.permissions === 'admin' ? 'blue-post' : ''; return ( @@ -133,7 +173,7 @@ export class Thread extends React.Component {
- {`${index + 1}. `}{index > 0 && 'Re: '}{this.state.thread!.title} + {`${reply.index! + 1}. `}{index > 0 && 'Re: '}{this.state.thread!.title} | {this.getTimeFormat(reply.inserted_at)}
@@ -160,21 +200,33 @@ export class Thread extends React.Component { }); } + renderPagingBg() { + return ( +
+ this.navigateHere(page)} + showArrows={true} + /> +
+ ); + } + render() { - if (!this.state.thread) { + const { editingReply, thread, replies, showEditor, quotedReply } = this.state; + + if (!thread) { return
; } - const replies = get(this.state, 'thread.replies'); - return ( - {this.state.showEditor && + {showEditor && this.onEditorClose(cancel)} - quotedReply={this.state.quotedReply} - editingReply={this.state.editingReply} + quotedReply={quotedReply} + editingReply={editingReply} /> }
@@ -182,7 +234,7 @@ export class Thread extends React.Component {
Topic: - | {this.getTimeFormat(this.state.thread!.inserted_at)} + | {this.getTimeFormat(thread!.inserted_at)}
{ />
-
+ {this.renderPagingBg()}
{replies && this.renderReplies()}
-
+ {this.renderPagingBg()} +
this.navigateForumIndex()}