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

client - admin update thread/reply for locked/hidden/sticky

This commit is contained in:
2018-01-25 22:43:56 -06:00
parent 605e4ba94b
commit ce151bc0c0
8 changed files with 171 additions and 86 deletions

View File

@@ -3,6 +3,7 @@ import { UserModel } from './user';
export interface ReplyModel {
content: string;
edited: boolean;
hidden: boolean;
id: number;
index?: number;
inserted_at: string;

View File

@@ -5,6 +5,7 @@ export interface ThreadModel {
category_id: number;
title: string;
edited: boolean;
hidden: boolean;
id: number;
inserted_at: string;
last_reply: UserModel;

View File

@@ -8,11 +8,6 @@ $grey2: #161616;
justify-content: space-between;
}
.forum-body {
min-height: 500px;
margin-bottom: 100px;
}
.forum-menu-search-bg {
background-image: url('../../assets/forum-menu-search-bg.gif');
@@ -36,6 +31,7 @@ $grey2: #161616;
.forum-table {
width: 100%;
border-spacing: 0;
border: 1px solid;
border-color: #575757;
color: #E2D9B0;
@@ -46,47 +42,69 @@ $grey2: #161616;
}
}
.forum-table__header {
background-image: url('../../assets/thread-topic-bg2.gif') !important;
background-repeat: repeat-x !important;
}
.forum-row {
display: flex;
align-items: center;
height: 24px;
background: $grey1;
&--header {
background-image: url('../../assets/thread-topic-bg2.gif');
background-repeat: repeat-x;
&__body {
height: 24px;
}
&--dark {
&:nth-child(even) {
background: $grey2;
}
}
.forum-cell {
height: 100%;
display: flex;
align-items: center;
padding: 0 2px;
padding: 2px;
&--header {
border: 1px solid;
border-color: #8F8F8F #8F8F8F #171511 #171511;
white-space: nowrap;
padding-right: 5px;
padding-left: 5px;
}
&--body {
border: 1px solid;
border-color: #000000 #000000 #252525 #252525;
border-color: #000000 #000000 #161616 #161616;
position: relative;
}
&--center {
justify-content: center;
text-align: center;
}
&--header-footer {
justify-content: space-between;
padding-right: 10px;
}
&__mod-controls {
display: none;
position: absolute;
top: -10px;
right: 5px;
background: #161616;
padding: 5px;
border: 1px solid $grey1;
a + a {
padding-left: 10px;
}
}
&:hover {
.forum-cell__mod-controls {
display: block;
}
}
}
.thread__title {

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Link, RouteComponentProps } from 'react-router-dom';
import { inject, observer } from 'mobx-react';
import { cloneDeep, filter, orderBy } from 'lodash';
import { ThreadService } from '../../services';
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';
@@ -84,7 +84,9 @@ export class Forum extends React.Component<Props, State> {
// fetch threads from server
private async getThreads(categoryId: number) {
const threads = await ThreadService.getCategoryThreads(categoryId);
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 });
this.processThreads(threads);
}
@@ -133,6 +135,11 @@ export class Forum extends React.Component<Props, State> {
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 });
@@ -161,7 +168,7 @@ export class Forum extends React.Component<Props, State> {
renderBody() {
return (
<div className="forum-body">
<div>
<form className="flex" style={{ marginBottom: 0 }} onSubmit={e => this.onSearch(e)}>
<img src={require('../../assets/forum-menu-left.gif')}/>
<img src={require('../../assets/forum-menu-newtopic.gif')}
@@ -183,35 +190,41 @@ export class Forum extends React.Component<Props, State> {
);
}
renderCell(content: JSX.Element | string, style: any, center?: boolean) {
let classNames: string = '';
classNames += center ? ' forum-cell--center' : '';
return <div className={`forum-cell flex-1 forum-cell--body ${classNames}`} style={style}>{content}</div>;
}
renderThreadRows() {
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 ? <img src={stickyImage} title="Sticky"/> : '';
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 ? <img src={stickyImage} title="Sticky"/> : '';
return (
<div className={`forum-row ${index % 2 === 0 && 'forum-row--dark'}`} key={index}>
{this.renderCell(sticky, { maxWidth: '50px' }, true)}
{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>{thread.reply_count}</b>, { maxWidth: '150px' }, true)}
{this.renderCell(<b>{thread.view_count}</b>, { maxWidth: '150px' }, true)}
{this.renderCell(
<tr className="forum-row forum-row__body" key={index}>
<td className={`forum-cell forum-cell--body forum-cell--center`}>{stickyElement}</td>
<td className={`forum-cell forum-cell--body`}>
<Link to={`/t/${categoryId}/${id}`} className="thread__title">{title}</Link>
{this.props.userStore.isModOrAdmin() &&
<span className="forum-cell__mod-controls">
<a onClick={() => this.onModItemClick({ id, sticky: !sticky })}>{sticky ? 'Unstick' : 'Stick'}</a>
<a onClick={() => this.onModItemClick({ id, locked: !locked })}>{locked ? 'Unlock' : 'Lock'}</a>
<a onClick={() => this.onModItemClick({ id, hidden: !hidden })}>{hidden ? 'Unhide' : 'Hide'}</a>
</span>
}
</td>
<td className={`forum-cell forum-cell--body`}>
<b className={authorBluePost}>{user.character_name || user.battletag}</b>
</td>
<td className={`forum-cell forum-cell--body forum-cell--center`}>
<b>{reply_count}</b>
</td>
<td className={`forum-cell forum-cell--body forum-cell--center`}>
<b>{view_count}</b>
</td>
<td className={`forum-cell forum-cell--body`}>
<div style={{ fontSize: '8pt' }}>
by <b className={lastReplyBluePost}>{thread.last_reply.character_name || thread.last_reply.battletag}</b>
</div>,
{ maxWidth: '200px' },
)}
</div>
by <b className={lastReplyBluePost}>{last_reply.character_name || last_reply.battletag}</b>
</div>
</td>
</tr>
);
});
}
@@ -233,22 +246,24 @@ export class Forum extends React.Component<Props, State> {
renderHeaderFooter() {
const { categoryId, sortBy, threadsPerPage, sortOrder } = this.routeParams();
return (
<div className="forum-row forum-row--header">
<div className="forum-cell forum-cell--header forum-cell--header-footer flex-1">
<div className="flex">
<span style={{ marginRight: '10px' }}>Page:</span>
<PaginationLinks
activePage={this.routeParams().page}
pageLinks={this.state.pageLinks}
onPageSelect={page => this.navigateHere(categoryId, page, threadsPerPage, sortBy, sortOrder)}
/>
<tr className="forum-table__header">
<td colSpan={100} className="forum-cell forum-cell--header">
<div className="flex forum-cell--header-footer">
<div className="flex">
<span style={{ marginRight: '10px' }}>Page:</span>
<PaginationLinks
activePage={this.routeParams().page}
pageLinks={this.state.pageLinks}
onPageSelect={page => this.navigateHere(categoryId, page, threadsPerPage, sortBy, sortOrder)}
/>
</div>
<div>
<b>Threads/Page:</b>
{this.renderThreadsPerPageDropdown()}
</div>
</div>
<div>
<b>Threads/Page:</b>
{this.renderThreadsPerPageDropdown()}
</div>
</div>
</div>
</td>
</tr>
);
}
@@ -257,49 +272,49 @@ export class Forum extends React.Component<Props, State> {
return show ? <img src={imgSrc}/> : null;
}
renderHeaderCell(columnHeader: ColumnHeader, style: any, center: boolean) {
renderHeaderCell(columnHeader: ColumnHeader, center: boolean) {
const { categoryId, page, threadsPerPage, sortBy, sortOrder } = this.routeParams();
const newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
const centerClass = center ? 'forum-cell--center' : '';
return (
<div className={`forum-cell forum-cell--header flex-1 ${centerClass}`} style={style}>
<td className={`forum-cell forum-cell--header ${centerClass}`}>
<a onClick={() => this.navigateHere(categoryId, page, threadsPerPage, columnHeader, newSortOrder)}>
<span>{columnHeader}</span>
{this.renderSortingArrow(sortBy === columnHeader, sortOrder)}
</a>
</div>
</td>
);
}
renderTable() {
return (
<div style={{ padding: '0 3px' }}>
<div className="forum-table">
<table className="forum-table">
<tbody>
{/* header */}
{this.renderHeaderFooter()}
{/* header */}
{this.renderHeaderFooter()}
{/* column headers */}
<div className="forum-row forum-row--header">
<div className={`forum-cell forum-cell--header flex-1 forum-cell--center`} style={{ maxWidth: '50px' }}>
<img src={require('../../assets/flag.gif')}/>
</div>
{this.renderHeaderCell(ColumnHeader.subject, { minWidth: '200px' }, false)}
{this.renderHeaderCell(ColumnHeader.author, { maxWidth: '150px' }, true)}
{this.renderHeaderCell(ColumnHeader.replies, { maxWidth: '150px' }, true)}
{this.renderHeaderCell(ColumnHeader.views, { maxWidth: '150px' }, true)}
{this.renderHeaderCell(ColumnHeader.lastPost, { maxWidth: '200px' }, true)}
</div>
<tr className="forum-table__header">
<td className={`forum-cell forum-cell--header forum-cell--center`} style={{ maxWidth: '50px' }}>
<img src={require('../../assets/flag.gif')}/>
</td>
{this.renderHeaderCell(ColumnHeader.subject, false)}
{this.renderHeaderCell(ColumnHeader.author, true)}
{this.renderHeaderCell(ColumnHeader.replies, true)}
{this.renderHeaderCell(ColumnHeader.views, true)}
{this.renderHeaderCell(ColumnHeader.lastPost, true)}
</tr>
{/* table body */}
{this.renderThreadRows()}
{/* body */}
{this.renderThreadRows()}
{/* footer */}
{this.renderHeaderFooter()}
</div>
{/* footer */}
{this.renderHeaderFooter()}
</tbody>
</table>
<div className="forumliner-bot-bg"/>
</div>
);

View File

@@ -70,7 +70,9 @@ export class Thread extends React.Component<Props, State> {
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; })
.map((r, i) => { r.index = i; return r; })
// remove hidden replies only for normal users
.reject(r => !this.props.userStore.isModOrAdmin() && r.hidden)
.value();
const { page } = this.routeParams(props);
const numPages = Math.ceil(thread.replies.length / 20);
@@ -114,6 +116,11 @@ export class Thread extends React.Component<Props, State> {
}
}
private async onModButtonClick(params: { id: number, hidden?: boolean}) {
await ThreadService.modUpdateReply(params);
this.getReplies();
}
private navigateForumIndex() {
this.props.history.push(`/f/${this.state.thread!.category_id}`);
}
@@ -161,6 +168,17 @@ export class Thread extends React.Component<Props, State> {
}
}
renderModButtons(reply: ReplyModel, index: number): any {
const { id, hidden } = reply;
if (index !== 0 && this.props.userStore.isModOrAdmin()) {
return (
<a style={{ paddingRight: '10px' }} onClick={() => this.onModButtonClick({ id, hidden: !hidden })}>
{hidden ? <span className="red">Unhide</span> : <span>Hide</span>}
</a>
);
}
}
renderReplies(): any {
return this.state.replies.map((reply, index) => {
const replyDark = index % 2 === 0 ? 'reply--dark' : '';
@@ -177,6 +195,7 @@ export class Thread extends React.Component<Props, State> {
<small style={{ paddingLeft: '5px' }}>| {this.getTimeFormat(reply.inserted_at)}</small>
</div>
<div className="flex flex--center">
{this.renderModButtons(reply, index)}
{this.renderEditbutton(reply)}
<img src={require('../../assets/quote-button.gif')}
className="reply__title__button"

View File

@@ -23,7 +23,34 @@ const getThread = async (thread_id: string | number): Promise<ThreadModel> => {
return [] as any;
};
export interface ModUpdate {
id: number;
sticky?: boolean;
locked?: boolean;
hidden?: boolean;
}
const modUpdateThread = async (params: ModUpdate): Promise<any> => {
try {
const res = await axios.put('/api/thread/mod', params);
return res.data.data;
} catch (e) {
console.log(e);
}
};
const modUpdateReply = async (params: { id: number, hidden?: boolean }): Promise<any> => {
try {
const res = await axios.put('/api/reply/mod', params);
return res.data.data;
} catch (e) {
console.log(e);
}
};
export const ThreadService = {
getCategoryThreads,
getThread,
modUpdateReply,
modUpdateThread,
};

View File

@@ -44,6 +44,9 @@ export class UserStore {
localStorage.removeItem('user');
}
@action isModOrAdmin() {
return this.user && this.user.permissions.match(/mod|admin/);
}
}
export default new UserStore();

View File

@@ -58,6 +58,7 @@ defmodule MyApp.Data.Thread do
:updated_at,
:inserted_at,
:sticky,
:hidden,
:locked,
:last_reply_id,
:category_id,