mirror of
https://github.com/mgerb/classic-wow-forums
synced 2026-01-10 17:12:48 +00:00
client - thread page pagination done
This commit is contained in:
BIN
client/app/assets/arrow-left.gif
Normal file
BIN
client/app/assets/arrow-left.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 456 B |
BIN
client/app/assets/arrow-right.gif
Normal file
BIN
client/app/assets/arrow-right.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 455 B |
@@ -0,0 +1,4 @@
|
|||||||
|
.pagination-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import './pagination-links.scss';
|
||||||
|
|
||||||
|
const arrows = {
|
||||||
|
right: require('../../assets/arrow-right.gif'),
|
||||||
|
left: require('../../assets/arrow-left.gif'),
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pageLinks: (number | string)[];
|
pageLinks: (number | string)[];
|
||||||
activePage: number;
|
activePage: number;
|
||||||
onPageSelect: (page: number) => void;
|
onPageSelect: (page: number) => void;
|
||||||
|
showArrows?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@@ -26,11 +33,26 @@ export class PaginationLinks extends React.Component<Props, State> {
|
|||||||
<a className="page-link" onClick={() => this.onPageSelect(page)}>{page}</a>;
|
<a className="page-link" onClick={() => this.onPageSelect(page)}>{page}</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 <img src={arrows[arrow]} onClick={() => this.onArrowClick(arrow)} className="clickable"/>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { activePage, pageLinks } = this.props;
|
const { activePage, pageLinks } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span className="pagination-links">
|
||||||
|
{this.renderArrow('left')}
|
||||||
{pageLinks.map((link, index) => {
|
{pageLinks.map((link, index) => {
|
||||||
const active = link === activePage;
|
const active = link === activePage;
|
||||||
return (
|
return (
|
||||||
@@ -43,6 +65,7 @@ export class PaginationLinks extends React.Component<Props, State> {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{this.renderArrow('right')}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface ReplyModel {
|
|||||||
content: string;
|
content: string;
|
||||||
edited: boolean;
|
edited: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
|
index?: number;
|
||||||
inserted_at: string;
|
inserted_at: string;
|
||||||
quote_id: number;
|
quote_id: number;
|
||||||
thread_id: number;
|
thread_id: number;
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ export class Forum extends React.Component<Props, State> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
threads,
|
threads,
|
||||||
pageThreads: [...threads].splice(threadIndex, threadsPerPage),
|
pageThreads: [...threads].splice(threadIndex, threadsPerPage),
|
||||||
pageLinks: pagination(page, numPages) },
|
pageLinks: pagination(page, numPages),
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private orderBy(threads: ThreadModel[], props: Props) {
|
private orderBy(threads: ThreadModel[], props: Props) {
|
||||||
@@ -235,7 +235,7 @@ export class Forum extends React.Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<div className="forum-row forum-row--header">
|
<div className="forum-row forum-row--header">
|
||||||
<div className="forum-cell forum-cell--header forum-cell--header-footer flex-1">
|
<div className="forum-cell forum-cell--header forum-cell--header-footer flex-1">
|
||||||
<div>
|
<div className="flex">
|
||||||
<span style={{ marginRight: '10px' }}>Page:</span>
|
<span style={{ marginRight: '10px' }}>Page:</span>
|
||||||
<PaginationLinks
|
<PaginationLinks
|
||||||
activePage={this.routeParams().page}
|
activePage={this.routeParams().page}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-body {
|
.reply-body {
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { get, find, orderBy } from 'lodash';
|
import { chain, get, find } from 'lodash';
|
||||||
import marked from 'marked';
|
import marked from 'marked';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { inject, observer } from 'mobx-react';
|
import { inject, observer } from 'mobx-react';
|
||||||
import { CharacterService, ThreadService } from '../../services';
|
import { CharacterService, ThreadService } from '../../services';
|
||||||
import { Editor, Portrait, ScrollToTop } from '../../components';
|
import { Editor, PaginationLinks, Portrait, ScrollToTop } from '../../components';
|
||||||
import { ReplyModel, ThreadModel } from '../../model';
|
import { ReplyModel, ThreadModel } from '../../model';
|
||||||
import { UserStore } from '../../stores/user-store';
|
import { UserStore } from '../../stores/user-store';
|
||||||
import { Oauth } from '../../util';
|
import { Oauth, pagination } from '../../util';
|
||||||
import './thread.scss';
|
import './thread.scss';
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<any> {
|
interface Props extends RouteComponentProps<any> {
|
||||||
userStore: UserStore;
|
userStore: UserStore;
|
||||||
|
page: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@@ -20,6 +21,8 @@ interface State {
|
|||||||
quotedReply?: ReplyModel;
|
quotedReply?: ReplyModel;
|
||||||
showEditor: boolean;
|
showEditor: boolean;
|
||||||
thread?: ThreadModel;
|
thread?: ThreadModel;
|
||||||
|
replies: ReplyModel[];
|
||||||
|
pageLinks: (number | string)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@inject('userStore')
|
@inject('userStore')
|
||||||
@@ -30,6 +33,8 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showEditor: false,
|
showEditor: false,
|
||||||
|
replies: [],
|
||||||
|
pageLinks: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +42,45 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
this.getReplies();
|
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() {
|
private async getReplies() {
|
||||||
const thread = await ThreadService.getThread(this.props.match.params['threadId']);
|
const thread = await ThreadService.getThread(this.props.match.params['threadId']);
|
||||||
thread.replies = orderBy(thread.replies, ['inserted_at']);
|
this.processReplies(thread);
|
||||||
this.setState({ 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() {
|
private onReplyClick() {
|
||||||
@@ -122,7 +162,7 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderReplies(): any {
|
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 replyDark = index % 2 === 0 ? 'reply--dark' : '';
|
||||||
const bluePost = reply.user.permissions === 'admin' ? 'blue-post' : '';
|
const bluePost = reply.user.permissions === 'admin' ? 'blue-post' : '';
|
||||||
return (
|
return (
|
||||||
@@ -133,7 +173,7 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
|
|
||||||
<div className="reply__title">
|
<div className="reply__title">
|
||||||
<div>
|
<div>
|
||||||
<b>{`${index + 1}. `}{index > 0 && 'Re: '}{this.state.thread!.title}</b>
|
<b>{`${reply.index! + 1}. `}{index > 0 && 'Re: '}{this.state.thread!.title}</b>
|
||||||
<small style={{ paddingLeft: '5px' }}>| {this.getTimeFormat(reply.inserted_at)}</small>
|
<small style={{ paddingLeft: '5px' }}>| {this.getTimeFormat(reply.inserted_at)}</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex--center">
|
<div className="flex flex--center">
|
||||||
@@ -160,21 +200,33 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderPagingBg() {
|
||||||
|
return (
|
||||||
|
<div className="paging-bg">
|
||||||
|
<PaginationLinks activePage={this.routeParams().page}
|
||||||
|
pageLinks={this.state.pageLinks}
|
||||||
|
onPageSelect={page => this.navigateHere(page)}
|
||||||
|
showArrows={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
if (!this.state.thread) {
|
const { editingReply, thread, replies, showEditor, quotedReply } = this.state;
|
||||||
|
|
||||||
|
if (!thread) {
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const replies = get(this.state, 'thread.replies');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollToTop {...this.props}>
|
<ScrollToTop {...this.props}>
|
||||||
{this.state.showEditor &&
|
{showEditor &&
|
||||||
<Editor threadId={this.props.match.params['threadId']}
|
<Editor threadId={this.props.match.params['threadId']}
|
||||||
onClose={cancel => this.onEditorClose(cancel)}
|
onClose={cancel => this.onEditorClose(cancel)}
|
||||||
quotedReply={this.state.quotedReply}
|
quotedReply={quotedReply}
|
||||||
editingReply={this.state.editingReply}
|
editingReply={editingReply}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<div className="topic-bg">
|
<div className="topic-bg">
|
||||||
@@ -182,7 +234,7 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
<div className="threadTopic">
|
<div className="threadTopic">
|
||||||
<img src={require('../../assets/sticky.gif')} style={{ marginRight: '5px' }}/>
|
<img src={require('../../assets/sticky.gif')} style={{ marginRight: '5px' }}/>
|
||||||
<b>Topic: </b>
|
<b>Topic: </b>
|
||||||
<small style={{ paddingLeft: '15px', color: 'white' }}>| {this.getTimeFormat(this.state.thread!.inserted_at)}</small>
|
<small style={{ paddingLeft: '15px', color: 'white' }}>| {this.getTimeFormat(thread!.inserted_at)}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img src={require('../../assets/forum-index.gif')}
|
<img src={require('../../assets/forum-index.gif')}
|
||||||
@@ -191,11 +243,12 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="paging-bg"/>
|
{this.renderPagingBg()}
|
||||||
<div className="reply-body">
|
<div className="reply-body">
|
||||||
{replies && this.renderReplies()}
|
{replies && this.renderReplies()}
|
||||||
</div>
|
</div>
|
||||||
<div className="paging-bg"/>
|
{this.renderPagingBg()}
|
||||||
|
|
||||||
<div className="forumliner-bot-bg"/>
|
<div className="forumliner-bot-bg"/>
|
||||||
<img src={require('../../assets/forum-index-bot.gif')}
|
<img src={require('../../assets/forum-index-bot.gif')}
|
||||||
onClick={() => this.navigateForumIndex()}
|
onClick={() => this.navigateForumIndex()}
|
||||||
|
|||||||
Reference in New Issue
Block a user