diff --git a/client/app/components/editor/editor.tsx b/client/app/components/editor/editor.tsx index d6002f9..8e09dbc 100644 --- a/client/app/components/editor/editor.tsx +++ b/client/app/components/editor/editor.tsx @@ -7,7 +7,7 @@ import { ReplyModel } from '../../model'; import './editor.scss'; interface Props { - categoryId?: string; + categoryId?: number; onClose: (cancel: boolean) => any; editingReply?: ReplyModel; quotedReply?: ReplyModel; diff --git a/client/app/components/index.ts b/client/app/components/index.ts index f21f738..6524799 100644 --- a/client/app/components/index.ts +++ b/client/app/components/index.ts @@ -4,5 +4,6 @@ export * from './footer/footer'; export * from './forum-nav/forum-nav'; export * from './header/header'; export * from './login-button/login-button'; +export * from './pagination-links/pagination-links'; export * from './portrait/portrait'; export * from './scroll-to-top/scroll-to-top'; diff --git a/client/app/components/pagination-links/pagination-links.tsx b/client/app/components/pagination-links/pagination-links.tsx new file mode 100644 index 0000000..067d637 --- /dev/null +++ b/client/app/components/pagination-links/pagination-links.tsx @@ -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 { + constructor(props: any) { + super(props); + } + + onPageSelect(page: number) { + this.props.onPageSelect(page); + window.scrollTo(0, 0); + } + + renderLink(page: number, active: boolean) { + return active ? + {page} : + this.onPageSelect(page)}>{page}; + } + + render() { + const { activePage, pageLinks } = this.props; + + return ( + + {pageLinks.map((link, index) => { + const active = link === activePage; + return ( + + {typeof link === 'number' ? + this.renderLink(link as number, active) : + . + } + {pageLinks.length !== index + 1 && .} + + ); + })} + + ); + } +} diff --git a/client/app/pages/forum/forum.scss b/client/app/pages/forum/forum.scss index 4558c8d..18da467 100644 --- a/client/app/pages/forum/forum.scss +++ b/client/app/pages/forum/forum.scss @@ -81,4 +81,17 @@ $grey2: #161616; &--center { justify-content: center; } + + + &--header-footer { + justify-content: space-between; + padding-right: 10px; + } +} + +.thread__title { + + &:visited { + color: #B1B1B1; + } } diff --git a/client/app/pages/forum/forum.tsx b/client/app/pages/forum/forum.tsx index bb3ae17..51ad3be 100644 --- a/client/app/pages/forum/forum.tsx +++ b/client/app/pages/forum/forum.tsx @@ -3,10 +3,10 @@ import { Link, RouteComponentProps } from 'react-router-dom'; import { inject, observer } from 'mobx-react'; import { orderBy } from 'lodash'; import { ThreadService } from '../../services'; -import { Editor, ForumNav, LoginButton, ScrollToTop } from '../../components'; +import { Editor, ForumNav, LoginButton, PaginationLinks, ScrollToTop } from '../../components'; import { ThreadModel } from '../../model'; import { UserStore } from '../../stores/user-store'; -import { Oauth } from '../../util'; +import { Oauth, pagination } from '../../util'; import './forum.scss'; const stickyImage = require('../../assets/sticky.gif'); @@ -18,6 +18,15 @@ interface Props extends RouteComponentProps { interface State { showEditor: boolean; threads: ThreadModel[]; + pageThreads: ThreadModel[]; + pageLinks: (number | string)[]; +} + +interface RouteParams { + categoryId: number; + page: number; + threadsPerPage: number; + sortBy: string; } @inject('userStore') @@ -28,31 +37,67 @@ export class Forum extends React.Component { this.state = { showEditor: false, 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() { - this.getThreads(this.props.match.params['id']); + this.getThreads(this.routeParams.categoryId); } // update the page if the route params change 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']); + } 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) { - let threads = await ThreadService.getCategoryThreads(categoryId); - threads = this.orderBy(threads); - this.setState({ threads }); + // fetch threads from server + private async getThreads(categoryId: number) { + const threads = await ThreadService.getCategoryThreads(categoryId); + 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[]) { 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) { this.setState({ showEditor: true }); } else { @@ -60,17 +105,17 @@ export class Forum extends React.Component { } } - onNewTopicClose(cancel: boolean) { + private onNewTopicClose(cancel: boolean) { this.setState({ showEditor: false }); if (!cancel) { - this.getThreads(this.props.match.params['id']); + this.getThreads(this.routeParams.categoryId); } } renderHeader() { return (
- +
this.props.history.push(dest)}/>
@@ -108,15 +153,18 @@ export class Forum extends React.Component { } renderThreadRows() { - const categoryId = this.props.match.params['id']; - return this.state.threads.map((thread, index) => { + const { categoryId } = this.routeParams; + return this.state.pageThreads.map((thread, index) => { const authorBluePost = thread.user.permissions === 'admin' ? 'blue' : ''; const lastReplyBluePost = thread.last_reply.permissions === 'admin' ? 'blue' : ''; const sticky = thread.sticky ? : ''; return (
{this.renderCell(sticky, { maxWidth: '50px' }, true)} - {this.renderCell({thread.title}, { minWidth: '200px' })} + {this.renderCell( + {thread.title}, + { minWidth: '200px' }, + )} {this.renderCell({thread.user.character_name || thread.user.battletag}, { maxWidth: '150px' })} {this.renderCell({thread.reply_count}, { maxWidth: '150px' }, true)} {this.renderCell({thread.view_count}, { maxWidth: '150px' }, true)} @@ -131,15 +179,67 @@ export class Forum extends React.Component { }); } + renderThreadsPerPageDropdown() { + const { categoryId, sortBy } = this.routeParams; + return ( + + ); + } + + // TOOD: + renderSortByDropdown() { + return ( + + ); + } + + renderHeaderFooter() { + const { categoryId, sortBy, threadsPerPage } = this.routeParams; + return ( +
+
+
+ Page: + this.navigateHere(categoryId, page, threadsPerPage, sortBy)} + /> +
+
+ Threads/Page: + {this.renderThreadsPerPageDropdown()} + Sort by: + {this.renderSortByDropdown()} +
+
+
+ ); + } + renderTable() { return (
-
-
TODO:
-
+ {/* header */} + {this.renderHeaderFooter()} + {/* column headers */}
{this.renderCell(, { maxWidth: '50px' }, true, true)} {this.renderCell(Subject, { minWidth: '200px' }, false, true)} @@ -149,7 +249,12 @@ export class Forum extends React.Component { {this.renderCell(Last Post, { maxWidth: '200px' }, true, true)}
+ {/* table body */} {this.renderThreadRows()} + + {/* footer */} + {this.renderHeaderFooter()} +
@@ -160,7 +265,7 @@ export class Forum extends React.Component { render() { return ( - {this.state.showEditor && this.onNewTopicClose(cancel)}/>} {this.renderHeader()} {this.renderBody()} diff --git a/client/app/routes.tsx b/client/app/routes.tsx index c6673ab..b280837 100644 --- a/client/app/routes.tsx +++ b/client/app/routes.tsx @@ -44,8 +44,8 @@ export class Routes extends React.Component { - - + + diff --git a/client/app/scss/style.scss b/client/app/scss/style.scss index 761a950..b83122b 100644 --- a/client/app/scss/style.scss +++ b/client/app/scss/style.scss @@ -33,9 +33,6 @@ a { color: white; } - &:visited { - color: #B1B1B1; - } } hr { @@ -178,3 +175,7 @@ div { color: $bluePost; } } + +.page-link { + margin: 0 5px; +} diff --git a/client/app/services/thread.service.ts b/client/app/services/thread.service.ts index c03b023..12714bc 100644 --- a/client/app/services/thread.service.ts +++ b/client/app/services/thread.service.ts @@ -1,7 +1,7 @@ import axios from '../axios/axios'; import { ThreadModel } from '../model'; -const getCategoryThreads = async (category_id: string): Promise => { +const getCategoryThreads = async (category_id: number): Promise => { try { const res = await axios.get(`/api/thread?category_id=${category_id}`); return res.data.data; diff --git a/client/app/util/index.ts b/client/app/util/index.ts index 1958c33..0debf92 100644 --- a/client/app/util/index.ts +++ b/client/app/util/index.ts @@ -1 +1,2 @@ export * from './oauth/oauth'; +export * from './pagination/pagination'; diff --git a/client/app/util/pagination/pagination.ts b/client/app/util/pagination/pagination.ts new file mode 100644 index 0000000..9a2ac1f --- /dev/null +++ b/client/app/util/pagination/pagination.ts @@ -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; +}; diff --git a/client/tslint.json b/client/tslint.json index 99dc48a..59ebfc8 100644 --- a/client/tslint.json +++ b/client/tslint.json @@ -4,6 +4,7 @@ "import-name": false, "max-line-length": [true, 140], "no-unused-variable": [true], - "variable-name": [false] + "variable-name": [false], + "no-increment-decrement": [false] } }