1
0
mirror of https://github.com/mgerb/classic-wow-forums synced 2026-01-11 09:32:51 +00:00

client - forum page - pagination / threads per page

This commit is contained in:
2018-01-18 22:41:43 -06:00
parent 857df18cbf
commit 0d3a92c28d
11 changed files with 228 additions and 27 deletions

View File

@@ -7,7 +7,7 @@ import { ReplyModel } from '../../model';
import './editor.scss'; import './editor.scss';
interface Props { interface Props {
categoryId?: string; categoryId?: number;
onClose: (cancel: boolean) => any; onClose: (cancel: boolean) => any;
editingReply?: ReplyModel; editingReply?: ReplyModel;
quotedReply?: ReplyModel; quotedReply?: ReplyModel;

View File

@@ -4,5 +4,6 @@ export * from './footer/footer';
export * from './forum-nav/forum-nav'; export * from './forum-nav/forum-nav';
export * from './header/header'; export * from './header/header';
export * from './login-button/login-button'; export * from './login-button/login-button';
export * from './pagination-links/pagination-links';
export * from './portrait/portrait'; export * from './portrait/portrait';
export * from './scroll-to-top/scroll-to-top'; export * from './scroll-to-top/scroll-to-top';

View File

@@ -0,0 +1,49 @@
import React from 'react';
interface Props {
pageLinks: (number | string)[];
activePage: number;
onPageSelect: (page: number) => void;
}
interface State {
}
export class PaginationLinks extends React.Component<Props, State> {
constructor(props: any) {
super(props);
}
onPageSelect(page: number) {
this.props.onPageSelect(page);
window.scrollTo(0, 0);
}
renderLink(page: number, active: boolean) {
return active ?
<b className="page-link">{page}</b> :
<a className="page-link" onClick={() => this.onPageSelect(page)}>{page}</a>;
}
render() {
const { activePage, pageLinks } = this.props;
return (
<span>
{pageLinks.map((link, index) => {
const active = link === activePage;
return (
<span key={index}>
{typeof link === 'number' ?
this.renderLink(link as number, active) :
<span>.</span>
}
{pageLinks.length !== index + 1 && <span>.</span>}
</span>
);
})}
</span>
);
}
}

View File

@@ -81,4 +81,17 @@ $grey2: #161616;
&--center { &--center {
justify-content: center; justify-content: center;
} }
&--header-footer {
justify-content: space-between;
padding-right: 10px;
}
}
.thread__title {
&:visited {
color: #B1B1B1;
}
} }

View File

@@ -3,10 +3,10 @@ import { Link, RouteComponentProps } from 'react-router-dom';
import { inject, observer } from 'mobx-react'; import { inject, observer } from 'mobx-react';
import { orderBy } from 'lodash'; import { orderBy } from 'lodash';
import { ThreadService } from '../../services'; import { ThreadService } from '../../services';
import { Editor, ForumNav, LoginButton, ScrollToTop } from '../../components'; import { Editor, ForumNav, LoginButton, PaginationLinks, ScrollToTop } from '../../components';
import { ThreadModel } from '../../model'; import { 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 './forum.scss'; import './forum.scss';
const stickyImage = require('../../assets/sticky.gif'); const stickyImage = require('../../assets/sticky.gif');
@@ -18,6 +18,15 @@ interface Props extends RouteComponentProps<any> {
interface State { interface State {
showEditor: boolean; showEditor: boolean;
threads: ThreadModel[]; threads: ThreadModel[];
pageThreads: ThreadModel[];
pageLinks: (number | string)[];
}
interface RouteParams {
categoryId: number;
page: number;
threadsPerPage: number;
sortBy: string;
} }
@inject('userStore') @inject('userStore')
@@ -28,31 +37,67 @@ export class Forum extends React.Component<Props, State> {
this.state = { this.state = {
showEditor: false, showEditor: false,
threads: [], threads: [],
pageThreads: [],
pageLinks: [],
};
}
// easier way to get route params - will provide default values if null
private get routeParams(): RouteParams {
return {
categoryId: parseInt(this.props.match.params['id'], 10),
page: parseInt(this.props.match.params['page'], 10) || 1,
threadsPerPage: parseInt(this.props.match.params['threadsPerPage'], 10) || 25,
sortBy: this.props.match.params['sortBy'] || 'Latest Reply',
}; };
} }
componentDidMount() { componentDidMount() {
this.getThreads(this.props.match.params['id']); this.getThreads(this.routeParams.categoryId);
} }
// update the page if the route params change // update the page if the route params change
componentWillReceiveProps(nextProps: Props) { componentWillReceiveProps(nextProps: Props) {
if (this.props.match.params !== nextProps.match.params) { if (this.props.match.params['id'] !== nextProps.match.params['id']) {
this.getThreads(nextProps.match.params['id']); this.getThreads(nextProps.match.params['id']);
} else {
// have to grab params from next props
const page = parseInt(nextProps.match.params['page'], 10) || 1;
const threadsPerPage = parseInt(nextProps.match.params['threadsPerPage'], 10) || 25;
this.processThreads(this.state.threads, page, threadsPerPage);
} }
} }
private async getThreads(categoryId: string) { // fetch threads from server
let threads = await ThreadService.getCategoryThreads(categoryId); private async getThreads(categoryId: number) {
threads = this.orderBy(threads); const threads = await ThreadService.getCategoryThreads(categoryId);
this.setState({ threads }); this.processThreads(threads, this.routeParams.page, this.routeParams.threadsPerPage);
} }
// process threads and set state
private processThreads(unorderedThreads: ThreadModel[], page: number, threadsPerPage: number): void {
const threads = this.orderBy(unorderedThreads);
const numPages = Math.ceil(threads.length / threadsPerPage);
const threadIndex = (page - 1) * threadsPerPage;
this.setState({
threads,
pageThreads: [...threads].splice(threadIndex, threadsPerPage),
pageLinks: pagination(page, numPages) },
);
}
// TODO:
private orderBy(threads: ThreadModel[]) { private orderBy(threads: ThreadModel[]) {
return orderBy(threads, ['sticky', 'updated_at'], ['desc', 'desc']); return orderBy(threads, ['sticky', 'updated_at'], ['desc', 'desc']);
} }
onNewTopic() { private navigateHere(categoryId: number, page: number, threadsPerPage: number, sortBy: string) {
const url = `/f/${categoryId}/${page}/${threadsPerPage}/${sortBy}`;
this.props.history.push(url);
}
private onNewTopic() {
if (this.props.userStore.user) { if (this.props.userStore.user) {
this.setState({ showEditor: true }); this.setState({ showEditor: true });
} else { } else {
@@ -60,17 +105,17 @@ export class Forum extends React.Component<Props, State> {
} }
} }
onNewTopicClose(cancel: boolean) { private onNewTopicClose(cancel: boolean) {
this.setState({ showEditor: false }); this.setState({ showEditor: false });
if (!cancel) { if (!cancel) {
this.getThreads(this.props.match.params['id']); this.getThreads(this.routeParams.categoryId);
} }
} }
renderHeader() { renderHeader() {
return ( return (
<div className="forum-header"> <div className="forum-header">
<ForumNav categoryId={parseInt(this.props.match.params['id'], 10)} {...this.props}/> <ForumNav categoryId={this.routeParams.categoryId} {...this.props}/>
<div style={{ height: '100%' }}> <div style={{ height: '100%' }}>
<LoginButton onNavigate={dest => this.props.history.push(dest)}/> <LoginButton onNavigate={dest => this.props.history.push(dest)}/>
</div> </div>
@@ -108,15 +153,18 @@ export class Forum extends React.Component<Props, State> {
} }
renderThreadRows() { renderThreadRows() {
const categoryId = this.props.match.params['id']; const { categoryId } = this.routeParams;
return this.state.threads.map((thread, index) => { return this.state.pageThreads.map((thread, index) => {
const authorBluePost = thread.user.permissions === 'admin' ? 'blue' : ''; const authorBluePost = thread.user.permissions === 'admin' ? 'blue' : '';
const lastReplyBluePost = thread.last_reply.permissions === 'admin' ? 'blue' : ''; const lastReplyBluePost = thread.last_reply.permissions === 'admin' ? 'blue' : '';
const sticky = thread.sticky ? <img src={stickyImage} title="Sticky"/> : ''; const sticky = thread.sticky ? <img src={stickyImage} title="Sticky"/> : '';
return ( return (
<div className={`forum-row ${index % 2 === 0 && 'forum-row--dark'}`} key={index}> <div className={`forum-row ${index % 2 === 0 && 'forum-row--dark'}`} key={index}>
{this.renderCell(sticky, { maxWidth: '50px' }, true)} {this.renderCell(sticky, { maxWidth: '50px' }, true)}
{this.renderCell(<Link to={`/f/${categoryId}/${thread.id}`}>{thread.title}</Link>, { minWidth: '200px' })} {this.renderCell(
<Link to={`/t/${categoryId}/${thread.id}`} className="thread__title">{thread.title}</Link>,
{ minWidth: '200px' },
)}
{this.renderCell(<b className={authorBluePost}>{thread.user.character_name || thread.user.battletag}</b>, { maxWidth: '150px' })} {this.renderCell(<b className={authorBluePost}>{thread.user.character_name || thread.user.battletag}</b>, { maxWidth: '150px' })}
{this.renderCell(<b>{thread.reply_count}</b>, { maxWidth: '150px' }, true)} {this.renderCell(<b>{thread.reply_count}</b>, { maxWidth: '150px' }, true)}
{this.renderCell(<b>{thread.view_count}</b>, { maxWidth: '150px' }, true)} {this.renderCell(<b>{thread.view_count}</b>, { maxWidth: '150px' }, true)}
@@ -131,15 +179,67 @@ export class Forum extends React.Component<Props, State> {
}); });
} }
renderThreadsPerPageDropdown() {
const { categoryId, sortBy } = this.routeParams;
return (
<select style={{ margin: '0 5px' }}
value={this.routeParams.threadsPerPage}
onChange={e => this.navigateHere(categoryId, 1, parseInt(e.target.value, 10), sortBy)}
>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={75}>75</option>
</select>
);
}
// TOOD:
renderSortByDropdown() {
return (
<select style={{ margin: '0 5px' }}>
<option>Latest Reply</option>
<option>Subject</option>
<option>Author</option>
<option># of Replies</option>
<option># of Views</option>
<option>Creation Date</option>
</select>
);
}
renderHeaderFooter() {
const { categoryId, sortBy, threadsPerPage } = this.routeParams;
return (
<div className="forum-row forum-row--header">
<div className="forum-cell forum-cell--header forum-cell--header-footer flex-1">
<div>
<span style={{ marginRight: '10px' }}>Page:</span>
<PaginationLinks
activePage={this.routeParams.page}
pageLinks={this.state.pageLinks}
onPageSelect={page => this.navigateHere(categoryId, page, threadsPerPage, sortBy)}
/>
</div>
<div>
<b>Threads/Page:</b>
{this.renderThreadsPerPageDropdown()}
<b>Sort by:</b>
{this.renderSortByDropdown()}
</div>
</div>
</div>
);
}
renderTable() { renderTable() {
return ( return (
<div style={{ padding: '0 3px' }}> <div style={{ padding: '0 3px' }}>
<div className="forum-table"> <div className="forum-table">
<div className="forum-row forum-row--header"> {/* header */}
<div className="forum-cell forum-cell--header flex-1">TODO:</div> {this.renderHeaderFooter()}
</div>
{/* column headers */}
<div className="forum-row forum-row--header"> <div className="forum-row forum-row--header">
{this.renderCell(<img src={require('../../assets/flag.gif')}/>, { maxWidth: '50px' }, true, true)} {this.renderCell(<img src={require('../../assets/flag.gif')}/>, { maxWidth: '50px' }, true, true)}
{this.renderCell(<a>Subject</a>, { minWidth: '200px' }, false, true)} {this.renderCell(<a>Subject</a>, { minWidth: '200px' }, false, true)}
@@ -149,7 +249,12 @@ export class Forum extends React.Component<Props, State> {
{this.renderCell(<a>Last Post</a>, { maxWidth: '200px' }, true, true)} {this.renderCell(<a>Last Post</a>, { maxWidth: '200px' }, true, true)}
</div> </div>
{/* table body */}
{this.renderThreadRows()} {this.renderThreadRows()}
{/* footer */}
{this.renderHeaderFooter()}
</div> </div>
<div className="forumliner-bot-bg"/> <div className="forumliner-bot-bg"/>
@@ -160,7 +265,7 @@ export class Forum extends React.Component<Props, State> {
render() { render() {
return ( return (
<ScrollToTop> <ScrollToTop>
{this.state.showEditor && <Editor categoryId={this.props.match.params['id']} {this.state.showEditor && <Editor categoryId={this.routeParams.categoryId}
onClose={cancel => this.onNewTopicClose(cancel)}/>} onClose={cancel => this.onNewTopicClose(cancel)}/>}
{this.renderHeader()} {this.renderHeader()}
{this.renderBody()} {this.renderBody()}

View File

@@ -44,8 +44,8 @@ export class Routes extends React.Component<Props, State> {
<Switch> <Switch>
<Route exact path="/" component={Home} /> <Route exact path="/" component={Home} />
<Route exact path="/realms" component={Realms} /> <Route exact path="/realms" component={Realms} />
<Route exact path="/f/:id" component={Forum} /> <Route exact path="/f/:id/:page?/:threadsPerPage?/:sortBy?" component={Forum} />
<Route exact path="/f/:categoryId/:threadId" component={Thread} /> <Route exact path="/t/:categoryId/:threadId/:page?" component={Thread} />
<Route exact path="/oauth" component={Oauth} /> <Route exact path="/oauth" component={Oauth} />
<Route exact path="/user-account" component={UserAccount} /> <Route exact path="/user-account" component={UserAccount} />
<Route exact path="/login" component={Login} /> <Route exact path="/login" component={Login} />

View File

@@ -33,9 +33,6 @@ a {
color: white; color: white;
} }
&:visited {
color: #B1B1B1;
}
} }
hr { hr {
@@ -178,3 +175,7 @@ div {
color: $bluePost; color: $bluePost;
} }
} }
.page-link {
margin: 0 5px;
}

View File

@@ -1,7 +1,7 @@
import axios from '../axios/axios'; import axios from '../axios/axios';
import { ThreadModel } from '../model'; import { ThreadModel } from '../model';
const getCategoryThreads = async (category_id: string): Promise<ThreadModel[]> => { const getCategoryThreads = async (category_id: number): Promise<ThreadModel[]> => {
try { try {
const res = await axios.get(`/api/thread?category_id=${category_id}`); const res = await axios.get(`/api/thread?category_id=${category_id}`);
return res.data.data; return res.data.data;

View File

@@ -1 +1,2 @@
export * from './oauth/oauth'; export * from './oauth/oauth';
export * from './pagination/pagination';

View File

@@ -0,0 +1,30 @@
export const pagination = (page: number, numPages: number): (number | string)[] => {
const current = page;
const last = numPages;
const delta = 2;
const left = current - delta;
const right = current + delta + 1;
const range = [];
const rangeWithDots = [];
let l;
for (let i = 1; i <= last; i++) {
if (i === 1 || i === last || (i >= left && i < right)) {
range.push(i);
}
}
for (const i of range) {
if (l) {
if (i - l === 2) {
rangeWithDots.push(l + 1);
} else if (i - l !== 1) {
rangeWithDots.push('...');
}
}
rangeWithDots.push(i);
l = i;
}
return rangeWithDots;
};

View File

@@ -4,6 +4,7 @@
"import-name": false, "import-name": false,
"max-line-length": [true, 140], "max-line-length": [true, 140],
"no-unused-variable": [true], "no-unused-variable": [true],
"variable-name": [false] "variable-name": [false],
"no-increment-decrement": [false]
} }
} }