import React from 'react'; import { Link, RouteComponentProps } from 'react-router-dom'; import { inject, observer } from 'mobx-react'; import { cloneDeep, filter, orderBy, reject } from 'lodash'; import { ThreadService, ModUpdate } from '../../services'; import { Editor, ForumNav, LoginButton, PaginationLinks, ScrollToTop } from '../../components'; import { ThreadModel } from '../../model'; import { UserStore } from '../../stores/user-store'; import { Oauth, pagination } from '../../util'; import './forum.scss'; const stickyImage = require('../../assets/sticky.gif'); const upArrow = require('../../assets/arrow-up.gif'); const downArrow = require('../../assets/arrow-down.gif'); interface Props extends RouteComponentProps { userStore: UserStore; } interface State { showEditor: boolean; threads: ThreadModel[]; threadsLoaded: boolean; initialThreads: ThreadModel[]; pageThreads: ThreadModel[]; pageLinks: (number | string)[]; searchText: string; } interface RouteParams { categoryId: number; page: number; threadsPerPage: number; sortBy: ColumnHeader; sortOrder: 'asc' | 'desc'; } // TODO: refactor this on back end to match UI enum ColumnHeader { subject = 'Subject', author = 'Author', replies = 'Replies', views = 'Views', lastPost= 'Last Post', } @inject('userStore') @observer export class Forum extends React.Component { constructor(props: Props) { super(props); this.state = { showEditor: false, threads: [], threadsLoaded: false, initialThreads: [], pageThreads: [], pageLinks: [], searchText: '', }; } // easier way to get route params - will provide default values if null private routeParams(props: Props = this.props): RouteParams { return { categoryId: parseInt(props.match.params['id'], 10), page: parseInt(props.match.params['page'], 10) || 1, threadsPerPage: parseInt(props.match.params['threadsPerPage'], 10) || 25, sortBy: props.match.params['sortBy'] || ColumnHeader.lastPost, sortOrder: props.match.params['sortOrder'] || 'desc', }; } componentDidMount() { this.getThreads(this.routeParams().categoryId); } // update the page if the route params change componentWillReceiveProps(nextProps: Props) { if (this.props.match.params['id'] !== nextProps.match.params['id']) { this.getThreads(nextProps.match.params['id']); } else { this.processThreads(this.state.threads, nextProps); } } // fetch threads from server private async getThreads(categoryId: number) { let threads = await ThreadService.getCategoryThreads(categoryId); // remove hidden threads from normal users threads = reject(threads, t => !this.props.userStore.isModOrAdmin() && t.hidden); this.setState({ initialThreads: threads, threadsLoaded: true }); this.processThreads(threads); } // process threads and set state private processThreads(unorderedThreads: ThreadModel[], props: Props = this.props): void { const { threadsPerPage, page } = this.routeParams(props); const threads = this.orderBy(unorderedThreads, props); const numPages = Math.ceil(threads.length / threadsPerPage); const threadIndex = (page - 1) * threadsPerPage; this.setState({ threads, pageThreads: [...threads].splice(threadIndex, threadsPerPage), pageLinks: pagination(page, numPages), }); } private orderBy(threads: ThreadModel[], props: Props) { const { sortBy, sortOrder } = this.routeParams(props); const titleMap: any = { [ColumnHeader.subject]: (t: any) => t.title.toLowerCase(), [ColumnHeader.author]: (t: any) => { return t.user.character_name ? t.user.character_name.toLowerCase() : t.user.battletag.toLowerCase(); }, [ColumnHeader.replies]: 'reply_count', [ColumnHeader.views]: 'view_count', [ColumnHeader.lastPost]: 'updated_at', }; // always sort sticky to top return orderBy(threads, ['sticky', titleMap[sortBy]], ['desc', sortOrder]); } private navigateHere(categoryId: number, page: number, threadsPerPage: number, sortBy: ColumnHeader, sortOrder: 'asc' | 'desc') { const url = `/f/${categoryId}/${page}/${threadsPerPage}/${sortBy}/${sortOrder}`; this.props.history.push(url); } private onSearch(event: any) { event.preventDefault(); const threads: any = filter(cloneDeep(this.state.initialThreads), (t) => { return t.title.toLowerCase().match(this.state.searchText.toLowerCase()); }); this.processThreads(threads); } private async onModItemClick(params: ModUpdate) { await ThreadService.modUpdateThread(params); this.getThreads(this.routeParams().categoryId); } private onNewTopic() { if (this.props.userStore.user) { this.setState({ showEditor: true }); } else { Oauth.openOuathWindow(); } } private onNewTopicClose(cancel: boolean) { this.setState({ showEditor: false }); if (!cancel) { this.getThreads(this.routeParams().categoryId); } } renderHeader() { return (
this.props.history.push(dest)}/>
); } renderBody() { return (
this.onSearch(e)}> this.onNewTopic()}/>
this.setState({ searchText: event.target.value })}/>
{this.renderTable()}
); } renderThreadRows() { const { categoryId } = this.routeParams(); return this.state.pageThreads.map((thread, index) => { const { id, sticky, hidden, last_reply, locked, reply_count, title, user, view_count } = thread; const authorBluePost = user.permissions === 'admin' ? 'blue' : ''; const lastReplyBluePost = last_reply.permissions === 'admin' ? 'blue' : ''; const stickyElement = sticky ? : ''; return ( {stickyElement} {title} {this.props.userStore.isModOrAdmin() && this.onModItemClick({ id, sticky: !sticky })}>{sticky ? 'Unstick' : 'Stick'} this.onModItemClick({ id, locked: !locked })}>{locked ? 'Unlock' : 'Lock'} this.onModItemClick({ id, hidden: !hidden })}>{hidden ? 'Unhide' : 'Hide'} } {user.character_name || user.battletag} {reply_count.toLocaleString()} {view_count.toLocaleString()}
by {last_reply.character_name || last_reply.battletag}
); }); } renderThreadsPerPageDropdown() { const { categoryId, sortBy, sortOrder } = this.routeParams(); return ( ); } renderHeaderFooter() { const { categoryId, sortBy, threadsPerPage, sortOrder } = this.routeParams(); return (
Page: this.navigateHere(categoryId, page, threadsPerPage, sortBy, sortOrder)} />
Threads/Page: {this.renderThreadsPerPageDropdown()}
); } renderSortingArrow(show: boolean, sortOrder: string) { const imgSrc = sortOrder === 'asc' ? upArrow : downArrow; return show ? : null; } renderHeaderCell(columnHeader: ColumnHeader, center: boolean, hideTiny?: boolean) { const { categoryId, page, threadsPerPage, sortBy, sortOrder } = this.routeParams(); const newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; const centerClass = center ? 'forum-cell--center' : ''; const hideClass = hideTiny ? 'hide-tiny' : ''; return ( this.navigateHere(categoryId, page, threadsPerPage, columnHeader, newSortOrder)}> {columnHeader} {this.renderSortingArrow(sortBy === columnHeader, sortOrder)} ); } renderTable() { const table = ( {/* header */} {this.renderHeaderFooter()} {this.renderHeaderCell(ColumnHeader.subject, false)} {this.renderHeaderCell(ColumnHeader.author, true)} {this.renderHeaderCell(ColumnHeader.replies, true)} {this.renderHeaderCell(ColumnHeader.views, true, true)} {this.renderHeaderCell(ColumnHeader.lastPost, true, true)} {/* body */} {this.renderThreadRows()} {/* footer */} {this.renderHeaderFooter()}
); const noThreadsMessage = (

There doesn't seem to be anything here. this.onNewTopic()}>Create the first topic!

); const { threads, threadsLoaded } = this.state; return (
{threadsLoaded && threads.length < 1 ? noThreadsMessage : table }
); } render() { return ( {this.state.showEditor && this.onNewTopicClose(cancel)}/>} {this.renderHeader()} {this.renderBody()} ); } }