mirror of
https://github.com/mgerb/classic-wow-forums
synced 2026-01-10 09:02:50 +00:00
little bit of everything - it's been a long day
This commit is contained in:
BIN
client/app/assets/avatars/unknown.gif
Normal file
BIN
client/app/assets/avatars/unknown.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
client/app/assets/search.gif
Normal file
BIN
client/app/assets/search.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 347 B |
@@ -2,10 +2,10 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: calc(100% - 96px);
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background-color: rgba(0, 0, 0, .5);
|
background-color: rgba(0, 0, 0, .9);
|
||||||
padding-top: 50px;
|
padding-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,21 +15,22 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
|
|
||||||
|
&__character-count {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
height: 25px;
|
height: 25px;
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__text-area {
|
&__text-area {
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
resize: none;
|
resize: none;
|
||||||
margin-bottom: 20px;
|
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__preview {
|
&__preview {
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
background-color: #161616;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ import { ContentContainer } from '../content-container/content-container';
|
|||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
categoryId: string;
|
categoryId?: string;
|
||||||
onClose: (cancel: boolean) => any;
|
onClose: (cancel: boolean) => any;
|
||||||
|
threadId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
contentPreview: string;
|
contentPreview: string;
|
||||||
characterCount: number;
|
contentCharacterCount: number;
|
||||||
|
titleCharacterCount: number;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +26,8 @@ export class Editor extends React.Component<Props, State> {
|
|||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
contentPreview: '',
|
contentPreview: '',
|
||||||
characterCount: 0,
|
contentCharacterCount: 0,
|
||||||
|
titleCharacterCount: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,12 +35,48 @@ export class Editor extends React.Component<Props, State> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
content: event.target.value,
|
content: event.target.value,
|
||||||
contentPreview: marked(event.target.value, { sanitize: true }),
|
contentPreview: marked(event.target.value, { sanitize: true }),
|
||||||
characterCount: event.target.value.length,
|
contentCharacterCount: event.target.value.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit() {
|
onTitleChange(event: any) {
|
||||||
|
this.setState({
|
||||||
|
title: event.target.value,
|
||||||
|
titleCharacterCount: event.target.value.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(event: any) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.props.threadId) {
|
||||||
|
this.newReply();
|
||||||
|
} else {
|
||||||
|
this.newThread();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async newReply() {
|
||||||
|
const { content } = this.state;
|
||||||
|
|
||||||
|
if (content === '') {
|
||||||
|
this.setState({ errorMessage: 'Content must not be blank.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
content,
|
||||||
|
thread_id: this.props.threadId,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post('/api/reply', data);
|
||||||
|
this.props.onClose(false);
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ errorMessage: 'Server error. Please try again later.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async newThread() {
|
||||||
const { title, content } = this.state;
|
const { title, content } = this.state;
|
||||||
|
|
||||||
if (title === '' || content === '') {
|
if (title === '' || content === '') {
|
||||||
@@ -51,10 +90,6 @@ export class Editor extends React.Component<Props, State> {
|
|||||||
category_id: this.props.categoryId,
|
category_id: this.props.categoryId,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newThread(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async newThread(data: any) {
|
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/thread', data);
|
await axios.post('/api/thread', data);
|
||||||
this.props.onClose(false);
|
this.props.onClose(false);
|
||||||
@@ -63,25 +98,46 @@ export class Editor extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderThreadPortion() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex--column">
|
||||||
|
<h2 style={{ color: 'white' }}>New Topic</h2>
|
||||||
|
<label>Title</label>
|
||||||
|
<input type="text"
|
||||||
|
className="input editor__title"
|
||||||
|
onChange={event => this.onTitleChange(event)}
|
||||||
|
maxLength={300}/>
|
||||||
|
<div className="editor__character-count">{this.state.contentCharacterCount}/2000</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: quote
|
||||||
|
renderReplyPortion() {
|
||||||
|
return (
|
||||||
|
<h2 style={{ color: 'white' }}>New Reply</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="editor-container">
|
<div className="editor-container">
|
||||||
|
<form onSubmit={event => this.onSubmit(event)} onReset={() => this.props.onClose(true)}>
|
||||||
<ContentContainer className="editor">
|
<ContentContainer className="editor">
|
||||||
<h2 style={{ color: 'white' }}>New Topic</h2>
|
{this.props.threadId ? this.renderReplyPortion() : this.renderThreadPortion()}
|
||||||
<label>Title</label>
|
|
||||||
<input type="text" className="input editor__title" onChange={event => this.setState({ title: event.target.value })}/>
|
|
||||||
<label>Content</label>
|
<label>Content</label>
|
||||||
<textarea className="input editor__text-area flex-1" onChange={event => this.onContentChange(event)}/>
|
<textarea className="input editor__text-area flex-1" onChange={event => this.onContentChange(event)} maxLength={2000}/>
|
||||||
<label>Content Preview</label>
|
<div className="editor__character-count">{this.state.contentCharacterCount}/2000</div>
|
||||||
|
<label>Preview</label>
|
||||||
<div className="editor__preview flex-1" dangerouslySetInnerHTML={{ __html: this.state.contentPreview }}></div>
|
<div className="editor__preview flex-1" dangerouslySetInnerHTML={{ __html: this.state.contentPreview }}></div>
|
||||||
<div className="editor__submit">
|
<div className="editor__submit">
|
||||||
<a onClick={() => this.onSubmit()}>Submit</a>
|
<input type="submit" value="Submit" className="input__button"/>
|
||||||
<a onClick={() => this.props.onClose(true)} style={{ marginLeft: '10px' }}>Cancel</a>
|
<input type="reset" value="Cancel" className="input__button" style={{ marginLeft: '10px' }}/>
|
||||||
<span className="editor__error-message">{this.state.errorMessage}</span>
|
<span className="editor__error-message">{this.state.errorMessage}</span>
|
||||||
<span style={{ float: 'right' }}>{this.state.characterCount}/2000</span>
|
|
||||||
</div>
|
</div>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
6
client/app/components/login-button/login-button.scss
Normal file
6
client/app/components/login-button/login-button.scss
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.portrait-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { Portrait } from '../portrait/portrait';
|
|||||||
import { UserStore } from '../../stores/user-store';
|
import { UserStore } from '../../stores/user-store';
|
||||||
import { CharacterService } from '../../services';
|
import { CharacterService } from '../../services';
|
||||||
import { Oauth } from '../../util';
|
import { Oauth } from '../../util';
|
||||||
|
import './login-button.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -24,7 +25,7 @@ export class LoginButton extends React.Component<Props, State> {
|
|||||||
renderPortrait() {
|
renderPortrait() {
|
||||||
const avatarSrc = CharacterService.getAvatar(this.props.userStore!.user!.character_avatar!);
|
const avatarSrc = CharacterService.getAvatar(this.props.userStore!.user!.character_avatar!);
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '10px' }}>
|
<div className="portrait-container">
|
||||||
<div onClick={() => this.props.onNavigate('/user-account')} style={{ cursor: 'pointer' }}>
|
<div onClick={() => this.props.onNavigate('/user-account')} style={{ cursor: 'pointer' }}>
|
||||||
{avatarSrc && <Portrait imageSrc={avatarSrc}/>}
|
{avatarSrc && <Portrait imageSrc={avatarSrc}/>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export interface RepyModel {
|
import { UserModel } from './user';
|
||||||
|
|
||||||
|
export interface ReplyModel {
|
||||||
content: string;
|
content: string;
|
||||||
edited: boolean;
|
edited: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
@@ -7,4 +9,5 @@ export interface RepyModel {
|
|||||||
thread_id: number;
|
thread_id: number;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
|
user: UserModel;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { RepyModel } from './reply';
|
import { ReplyModel } from './reply';
|
||||||
|
import { UserModel } from './user';
|
||||||
|
|
||||||
export interface ThreadModel {
|
export interface ThreadModel {
|
||||||
category_id: number;
|
category_id: number;
|
||||||
content: string;
|
|
||||||
title: string;
|
title: string;
|
||||||
edited: boolean;
|
edited: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
inserted_at: string;
|
inserted_at: string;
|
||||||
last_reply: { id: number; battletag: string };
|
last_reply: UserModel;
|
||||||
last_reply_id: number;
|
last_reply_id: number;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
replies: RepyModel[];
|
replies: ReplyModel[];
|
||||||
reply_count: number;
|
reply_count: number;
|
||||||
sticky: boolean;
|
sticky: boolean;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
user: { id: number; battletag: string };
|
user: UserModel;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
view_count: number;
|
view_count: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||||
import { get } from 'lodash';
|
|
||||||
import { inject, observer } from 'mobx-react';
|
import { inject, observer } from 'mobx-react';
|
||||||
import { ThreadService } from '../../services';
|
import { ThreadService } from '../../services';
|
||||||
import { Editor, ForumNav, LoginButton, ScrollToTop } from '../../components';
|
import { Editor, ForumNav, LoginButton, ScrollToTop } from '../../components';
|
||||||
@@ -107,10 +106,13 @@ export class Forum extends React.Component<Props, State> {
|
|||||||
<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('flag', { maxWidth: '50px' })}
|
{this.renderCell('flag', { maxWidth: '50px' })}
|
||||||
{this.renderCell(<Link to={`/f/${categoryId}/${thread.id}`}>{thread.title}</Link>, { minWidth: '200px' })}
|
{this.renderCell(<Link to={`/f/${categoryId}/${thread.id}`}>{thread.title}</Link>, { minWidth: '200px' })}
|
||||||
{this.renderCell(<b>{thread.user.battletag}</b>, { maxWidth: '150px' })}
|
{this.renderCell(<b>{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)}
|
||||||
{this.renderCell(<span>by <b>{get(thread, 'last_reply.battletag')}</b></span>, { maxWidth: '200px' })}
|
{this.renderCell(
|
||||||
|
<span>by <b>{thread.last_reply.character_name || thread.last_reply.battletag}</b></span>,
|
||||||
|
{ maxWidth: '200px' },
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export class Home extends React.Component<Props, State> {
|
|||||||
{this.renderTopic(139, 'Bug Report Forum', bugs, `Found a bug on this site? Help us squash it by reporting it here!`)}
|
{this.renderTopic(139, 'Bug Report Forum', bugs, `Found a bug on this site? Help us squash it by reporting it here!`)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr className="hr"/>
|
||||||
|
|
||||||
<div className="topic-row topic-row__classes">
|
<div className="topic-row topic-row__classes">
|
||||||
<div className="topic-item topic-item__classes">
|
<div className="topic-item topic-item__classes">
|
||||||
@@ -193,7 +193,7 @@ export class Home extends React.Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr className="hr"/>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</ScrollToTop>
|
</ScrollToTop>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,8 +46,11 @@
|
|||||||
|
|
||||||
.reply {
|
.reply {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
background-color: rgb(37, 37, 37);
|
||||||
|
|
||||||
|
&--dark {
|
||||||
background-color: rgb(22, 22, 22);
|
background-color: rgb(22, 22, 22);
|
||||||
// border: 1px solid #343434;
|
}
|
||||||
|
|
||||||
&__user-container {
|
&__user-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -89,3 +92,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.character-name {
|
||||||
|
color: #FFAC04;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import React from 'react';
|
|||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { get, map } from 'lodash';
|
import { get, map } from 'lodash';
|
||||||
import marked from 'marked';
|
import marked from 'marked';
|
||||||
import { ThreadService } from '../../services';
|
import { DateTime } from 'luxon';
|
||||||
import { Portrait, ScrollToTop } from '../../components';
|
import { CharacterService, ThreadService } from '../../services';
|
||||||
import { ThreadModel } from '../../model';
|
import { Editor, Portrait, ScrollToTop } from '../../components';
|
||||||
|
import { ReplyModel, ThreadModel } from '../../model';
|
||||||
import './thread.scss';
|
import './thread.scss';
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<any> {}
|
interface Props extends RouteComponentProps<any> {}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
showEditor: boolean;
|
||||||
thread?: ThreadModel;
|
thread?: ThreadModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,41 +19,89 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {};
|
this.state = {
|
||||||
|
showEditor: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.getThreads();
|
this.getReplies();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getThreads() {
|
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 = map([thread as any, ...thread.replies], (reply) => { // add the thread topic to the front of the list
|
thread.replies = map(thread.replies, (reply) => { // add the thread topic to the front of the list
|
||||||
reply.content = marked(reply.content, { sanitize: true });
|
reply.content = marked(reply.content, { sanitize: true });
|
||||||
return reply;
|
return reply;
|
||||||
});
|
});
|
||||||
this.setState({ thread });
|
this.setState({ thread });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onReplyClick() {
|
||||||
|
this.setState({ showEditor: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private onQuoteClick(reply: ReplyModel) {
|
||||||
|
console.log(reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEditorClose(cancel: boolean) {
|
||||||
|
this.setState({ showEditor: false });
|
||||||
|
if (!cancel) {
|
||||||
|
this.getReplies();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private navigateForumIndex() {
|
||||||
|
this.props.history.push(`/f/${this.state.thread!.category_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTimeFormat(dateTime: string) {
|
||||||
|
return DateTime.fromISO(dateTime).toLocaleString({
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
timeZoneName: 'short',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderUserInfo(reply: ReplyModel) {
|
||||||
|
const { battletag, character_avatar, character_class, character_guild, character_name, character_realm } = reply.user;
|
||||||
|
return (
|
||||||
|
<div className="reply__user-container">
|
||||||
|
<Portrait imageSrc={CharacterService.getAvatar(character_avatar)}/>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<div className="character-name">{character_name || battletag}</div>
|
||||||
|
{character_class && <div><small>{character_class}</small></div>}
|
||||||
|
{character_guild && <div><small><b>Guild: </b>{character_guild}</small></div>}
|
||||||
|
{character_realm && <div><small><b>Realm: </b>{character_realm}</small></div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderReplies(): any {
|
renderReplies(): any {
|
||||||
return this.state.thread!.replies.map((reply, index) => {
|
return this.state.thread!.replies.map((reply, index) => {
|
||||||
|
const replyDark = index % 2 === 0 ? 'reply--dark' : '';
|
||||||
return (
|
return (
|
||||||
<div className="reply-container" key={index}>
|
<div className="reply-container" key={index}>
|
||||||
<div className="reply">
|
<div className={`reply ${replyDark}`}>
|
||||||
<div className="reply__user-container">
|
{this.renderUserInfo(reply)}
|
||||||
<Portrait imageSrc={require('../../assets/Tyren.gif')}/>
|
|
||||||
<div>Tyren</div>
|
|
||||||
<div>Blizzard Poster</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="reply__title">
|
<div className="reply__title">
|
||||||
<div>
|
<div>
|
||||||
<b>{`${index + 1}. `}{this.state.thread!.title}</b>
|
<b>{`${index + 1}. `}{index > 0 && 'Re: '}{this.state.thread!.title}</b>
|
||||||
<small style={{ paddingLeft: '5px' }}>| {reply.inserted_at}</small>
|
<small style={{ paddingLeft: '5px' }}>| {this.getTimeFormat(reply.inserted_at)}</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<img src={require('../../assets/quote-button.gif')} className="reply__title__button"/>
|
<img src={require('../../assets/quote-button.gif')}
|
||||||
<img src={require('../../assets/reply-button.gif')} className="reply__title__button"/>
|
className="reply__title__button"
|
||||||
|
onClick={() => this.onQuoteClick(reply)}/>
|
||||||
|
<img src={require('../../assets/reply-button.gif')}
|
||||||
|
className="reply__title__button"
|
||||||
|
onClick={() => this.onReplyClick()}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="reply__content" dangerouslySetInnerHTML={{ __html: reply.content }}/>
|
<div className="reply__content" dangerouslySetInnerHTML={{ __html: reply.content }}/>
|
||||||
@@ -62,22 +112,23 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private navigateForumIndex() {
|
|
||||||
this.props.history.push(`/f/${this.state.thread!.category_id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
|
if (!this.state.thread) {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
const replies = get(this.state, 'thread.replies');
|
const replies = get(this.state, 'thread.replies');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollToTop {...this.props}>
|
<ScrollToTop {...this.props}>
|
||||||
|
{this.state.showEditor && <Editor threadId={this.props.match.params['threadId']} onClose={cancel => this.onEditorClose(cancel)}/>}
|
||||||
<div className="topic-bg">
|
<div className="topic-bg">
|
||||||
<div className="threadTopic-container">
|
<div className="threadTopic-container">
|
||||||
<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><small style={{ paddingLeft: '15px', color: 'white' }}>| 12/20/2005 1:11:44 AM PST</small>
|
<b>Topic: </b>
|
||||||
|
<small style={{ paddingLeft: '15px', color: 'white' }}>| {this.getTimeFormat(this.state.thread!.inserted_at)}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img src={require('../../assets/forum-index.gif')}
|
<img src={require('../../assets/forum-index.gif')}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export class UserAccount extends React.Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<ScrollToTop>
|
<ScrollToTop>
|
||||||
<ContentContainer style={{ minHeight: '500px', paddingTop: '40px' }}>
|
<ContentContainer style={{ minHeight: '500px', paddingTop: '40px' }}>
|
||||||
<div className="flex">
|
<div className="flex" style={{ marginBottom: '20px' }}>
|
||||||
{character_avatar && <Portrait imageSrc={CharacterService.getAvatar(character_avatar!)}/>}
|
{character_avatar && <Portrait imageSrc={CharacterService.getAvatar(character_avatar!)}/>}
|
||||||
<div style={{ paddingLeft: '10px' }}>
|
<div style={{ paddingLeft: '10px' }}>
|
||||||
{battletag && <div><b>Battletag: </b>{battletag}</div>}
|
{battletag && <div><b>Battletag: </b>{battletag}</div>}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
$fontPrimary: #cccccc;
|
$fontPrimary: #cccccc;
|
||||||
|
$linkColor: #FFB019;
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: Arial,Helvetica,Sans-Serif;
|
font-family: Arial,Helvetica,Sans-Serif;
|
||||||
@@ -19,7 +20,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #FFB019;
|
color: $linkColor;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 9pt;
|
font-size: 9pt;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@@ -35,6 +36,20 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
|
size: 1;
|
||||||
|
background-color: #9E9E9E;
|
||||||
|
display: block;
|
||||||
|
unicode-bidi: isolate;
|
||||||
|
-webkit-margin-before: 0.5em;
|
||||||
|
-webkit-margin-after: 0.5em;
|
||||||
|
-webkit-margin-start: auto;
|
||||||
|
-webkit-margin-end: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
border: none;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr {
|
||||||
background: rgb(71, 71, 71);
|
background: rgb(71, 71, 71);
|
||||||
width: 80%;
|
width: 80%;
|
||||||
height: 0.5px;
|
height: 0.5px;
|
||||||
@@ -44,6 +59,10 @@ hr {
|
|||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.inline-block {
|
.inline-block {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@@ -58,6 +77,10 @@ hr {
|
|||||||
&--wrap {
|
&--wrap {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--column {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-1 {
|
.flex-1 {
|
||||||
@@ -89,4 +112,18 @@ span.grey {
|
|||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
text-decoration: underline;
|
||||||
|
color: $linkColor;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 9pt;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { find, filter, get } from 'lodash';
|
import { find, filter, get } from 'lodash';
|
||||||
import { AvatarModel } from '../model';
|
import { AvatarModel } from '../model';
|
||||||
|
|
||||||
const getAvatar = (title: string): any => {
|
const getAvatar = (title?: string): any => {
|
||||||
const av = find(avatarList, { title });
|
const av = find(avatarList, { title });
|
||||||
return get(av, 'imageSrc');
|
return get(av, 'imageSrc') || avatarList[0].imageSrc;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClass = (index: number): {id: number, name: string; races: number[] } => {
|
const getClass = (index: number): {id: number, name: string; races: number[] } => {
|
||||||
@@ -66,6 +66,11 @@ const classList = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const avatarList: AvatarModel[] = [
|
const avatarList: AvatarModel[] = [
|
||||||
|
{
|
||||||
|
raceId: 0,
|
||||||
|
title: 'unknown',
|
||||||
|
imageSrc: require('../assets/avatars/unknown.gif'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
raceId: 1,
|
raceId: 1,
|
||||||
title: 'dwarf_f',
|
title: 'dwarf_f',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/fingerprintjs2": "^1.5.1",
|
"@types/fingerprintjs2": "^1.5.1",
|
||||||
"@types/lodash": "^4.14.92",
|
"@types/lodash": "^4.14.92",
|
||||||
|
"@types/luxon": "^0.2.2",
|
||||||
"@types/marked": "^0.3.0",
|
"@types/marked": "^0.3.0",
|
||||||
"@types/node": "^9.3.0",
|
"@types/node": "^9.3.0",
|
||||||
"@types/query-string": "^5.0.1",
|
"@types/query-string": "^5.0.1",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"html-webpack-plugin": "^2.30.1",
|
"html-webpack-plugin": "^2.30.1",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
|
"luxon": "^0.3.1",
|
||||||
"marked": "^0.3.12",
|
"marked": "^0.3.12",
|
||||||
"mobx": "^3.4.1",
|
"mobx": "^3.4.1",
|
||||||
"mobx-react": "^4.3.5",
|
"mobx-react": "^4.3.5",
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
version "4.14.92"
|
version "4.14.92"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.92.tgz#6e3cb0b71a1e12180a47a42a744e856c3ae99a57"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.92.tgz#6e3cb0b71a1e12180a47a42a744e856c3ae99a57"
|
||||||
|
|
||||||
|
"@types/luxon@^0.2.2":
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-0.2.2.tgz#ca83c1026980b111254ef78587ee027483e57df4"
|
||||||
|
|
||||||
"@types/marked@^0.3.0":
|
"@types/marked@^0.3.0":
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524"
|
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524"
|
||||||
@@ -3665,6 +3669,10 @@ lru-cache@^4.0.1:
|
|||||||
pseudomap "^1.0.2"
|
pseudomap "^1.0.2"
|
||||||
yallist "^2.1.2"
|
yallist "^2.1.2"
|
||||||
|
|
||||||
|
luxon@^0.3.1:
|
||||||
|
version "0.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/luxon/-/luxon-0.3.1.tgz#627f78f5fea94ac19e1cf2900f398a9338422bbd"
|
||||||
|
|
||||||
macaddress@^0.2.8:
|
macaddress@^0.2.8:
|
||||||
version "0.2.8"
|
version "0.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
|
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ defmodule MyApp.Data.Reply do
|
|||||||
field :content, :string
|
field :content, :string
|
||||||
field :edited, :boolean, default: false
|
field :edited, :boolean, default: false
|
||||||
field :quote_id, :integer
|
field :quote_id, :integer
|
||||||
timestamps()
|
has_one :user, Data.User, foreign_key: :id, references: :user_id
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp insert_changeset(reply, params \\ %{}) do
|
defp insert_changeset(reply, params \\ %{}) do
|
||||||
reply
|
reply
|
||||||
|> cast(params, [:user_id, :thread_id, :content, :quote])
|
|> cast(params, [:user_id, :thread_id, :content, :quote_id])
|
||||||
|> validate_required([:user_id, :thread_id, :content])
|
|> validate_required([:user_id, :thread_id, :content])
|
||||||
|> foreign_key_constraint(:user_id)
|
|> foreign_key_constraint(:user_id)
|
||||||
|> foreign_key_constraint(:thread_id)
|
|> foreign_key_constraint(:thread_id)
|
||||||
@@ -33,10 +34,11 @@ defmodule MyApp.Data.Reply do
|
|||||||
|
|
||||||
@spec insert(map) :: {:ok, map} | {:error, map}
|
@spec insert(map) :: {:ok, map} | {:error, map}
|
||||||
def insert(params) do
|
def insert(params) do
|
||||||
insert_changeset(%Data.Reply{}, params)
|
{:ok, data} = insert_changeset(%Data.Reply{}, params)
|
||||||
|> Repo.insert
|
|> Repo.insert
|
||||||
|> Data.Util.process_insert_or_update
|
|> Data.Util.process_insert_or_update
|
||||||
|> update_thread_new_reply
|
|> update_thread_new_reply
|
||||||
|
{:ok, Map.drop(data, [:user])} # drop user because we can't encode it if it's not preloaded
|
||||||
end
|
end
|
||||||
|
|
||||||
defp update_thread_new_reply({:error, error}), do: {:error, error}
|
defp update_thread_new_reply({:error, error}), do: {:error, error}
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ defmodule MyApp.Data.Thread do
|
|||||||
field :sticky, :boolean, default: false
|
field :sticky, :boolean, default: false
|
||||||
field :locked, :boolean, default: false
|
field :locked, :boolean, default: false
|
||||||
field :edited, :boolean, default: false
|
field :edited, :boolean, default: false
|
||||||
field :reply_count, :integer, default: 1
|
field :reply_count, :integer, default: 0
|
||||||
has_many :replies, Data.Reply
|
has_many :replies, Data.Reply
|
||||||
has_one :user, Data.User, foreign_key: :id, references: :user_id
|
has_one :user, Data.User, foreign_key: :id, references: :user_id
|
||||||
has_one :last_reply, Data.User, foreign_key: :id, references: :last_reply_id
|
has_one :last_reply, Data.User, foreign_key: :id, references: :last_reply_id
|
||||||
timestamps()
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp insert_changeset(thread, params \\ %{}) do
|
defp insert_changeset(thread, params \\ %{}) do
|
||||||
@@ -39,7 +39,7 @@ defmodule MyApp.Data.Thread do
|
|||||||
def get(thread_id) do
|
def get(thread_id) do
|
||||||
query = from t in Data.Thread,
|
query = from t in Data.Thread,
|
||||||
where: t.id == ^thread_id,
|
where: t.id == ^thread_id,
|
||||||
preload: [:user, :last_reply, :replies]
|
preload: [:user, :last_reply, replies: :user]
|
||||||
|
|
||||||
Repo.one(query)
|
Repo.one(query)
|
||||||
|> process_get
|
|> process_get
|
||||||
@@ -79,18 +79,20 @@ defmodule MyApp.Data.Thread do
|
|||||||
|
|
||||||
@spec insert(map) :: {:ok, map} | {:error, map}
|
@spec insert(map) :: {:ok, map} | {:error, map}
|
||||||
def insert(params) do
|
def insert(params) do
|
||||||
Repo.transaction(fn ->
|
{_, data} = Repo.transaction(fn ->
|
||||||
params = Map.put(params, "last_reply_id", Map.get(params, "user_id"))
|
params = Map.put(params, "last_reply_id", Map.get(params, "user_id"))
|
||||||
{:ok, thread} = insert_changeset(%Data.Thread{}, params)
|
{:ok, thread} = insert_changeset(%Data.Thread{}, params)
|
||||||
|> Repo.insert
|
|> Repo.insert
|
||||||
|
|
||||||
{:ok, _} = Repo.insert(%Data.Reply{
|
{:ok, data} = Repo.insert(%Data.Reply{
|
||||||
thread_id: Map.get(thread, :id),
|
thread_id: Map.get(thread, :id),
|
||||||
content: Map.get(params, "content"),
|
content: Map.get(params, "content"),
|
||||||
user_id: Map.get(params, "user_id")
|
user_id: Map.get(params, "user_id")
|
||||||
})
|
})
|
||||||
"ok"
|
# return the new thread we inserted - drop associations because we don't load them
|
||||||
|
{:ok, Map.drop(thread, [:last_reply, :replies, :user])}
|
||||||
end)
|
end)
|
||||||
|
data
|
||||||
end
|
end
|
||||||
|
|
||||||
# this doesn't update the 'updated_at' field which is what we want
|
# this doesn't update the 'updated_at' field which is what we want
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ defmodule MyApp.Data.User do
|
|||||||
field :character_class, :string
|
field :character_class, :string
|
||||||
field :character_realm, :string
|
field :character_realm, :string
|
||||||
field :character_avatar, :string
|
field :character_avatar, :string
|
||||||
timestamps()
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp changeset(user, params \\ %{}) do
|
defp changeset(user, params \\ %{}) do
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ defmodule MyApp.Repo.Migrations.CreateThread do
|
|||||||
|
|
||||||
def change do
|
def change do
|
||||||
create table(:thread) do
|
create table(:thread) do
|
||||||
add :title, :string
|
add :title, :string, size: 300
|
||||||
add :category_id, :integer
|
add :category_id, :integer
|
||||||
add :view_count, :integer
|
add :view_count, :integer
|
||||||
add :user_id, references(:user)
|
add :user_id, references(:user)
|
||||||
|
|||||||
@@ -13,18 +13,19 @@ defmodule MyApp.Data.ThreadTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "insert: try to insert with no parameters" do
|
test "insert: try to insert with no parameters" do
|
||||||
assert insert(%{}) == {:error,
|
{error, _} = catch_error(insert(%{}))
|
||||||
[%{title: "can't be blank"}, %{category_id: "can't be blank"},
|
assert error == :badmatch
|
||||||
%{content: "can't be blank"}, %{user_id: "can't be blank"}]}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "insert: insert as invalid user" do
|
test "insert: insert as invalid user" do
|
||||||
assert insert(new_thread(9238748)) == {:error, [%{user_id: "does not exist"}]}
|
{:badmatch, {:error, data}} = catch_error(insert(new_thread(9238748)))
|
||||||
|
assert data.errors == [user_id: {"does not exist", []}]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "insert: insert as invalid category_id" do
|
test "insert: insert as invalid category_id" do
|
||||||
{:ok, user} = new_user()
|
{:ok, user} = new_user()
|
||||||
assert insert(new_thread(user.id, 2342342343)) == {:error, [%{category_id: "does not exist"}]}
|
{error, _} = catch_error(insert(new_thread(user.id, 2342342343)))
|
||||||
|
assert error == :badmatch
|
||||||
end
|
end
|
||||||
|
|
||||||
test "new thread should be inserted" do
|
test "new thread should be inserted" do
|
||||||
@@ -33,7 +34,6 @@ defmodule MyApp.Data.ThreadTest do
|
|||||||
assert thread.title == "test title"
|
assert thread.title == "test title"
|
||||||
assert thread.category_id == 1
|
assert thread.category_id == 1
|
||||||
assert thread.user_id == user.id
|
assert thread.user_id == user.id
|
||||||
assert thread.content == "test content"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: update thread
|
# TODO: update thread
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ defmodule MyApp.Data.UserTest do
|
|||||||
battletag: "mgerb",
|
battletag: "mgerb",
|
||||||
id: user.id,
|
id: user.id,
|
||||||
permissions: "user",
|
permissions: "user",
|
||||||
|
character_avatar: nil,
|
||||||
|
character_class: nil,
|
||||||
|
character_guild: nil,
|
||||||
|
character_name: nil,
|
||||||
|
character_realm: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,21 +7,14 @@ defmodule MyAppWeb.ReplyControllerTest do
|
|||||||
new_conn = build_conn()
|
new_conn = build_conn()
|
||||||
|> put_req_header("authorization", "Bearer " <> user.token)
|
|> put_req_header("authorization", "Bearer " <> user.token)
|
||||||
|
|
||||||
conn = post(new_conn, "/api/reply")
|
{:badmatch, {:error, data}} = conn = catch_error(post(new_conn, "/api/reply"))
|
||||||
body = conn |> response(400) |> Poison.decode!
|
assert data == [%{thread_id: "can't be blank"}, %{content: "can't be blank"}]
|
||||||
|
|
||||||
assert body["error"]["message"] == [
|
{:badmatch, {:error, data}} = conn = catch_error(post(new_conn, "/api/reply", %{"content" => "t"}))
|
||||||
%{"thread_id" => "can't be blank"},
|
assert data == [%{thread_id: "can't be blank"}]
|
||||||
%{"content" => "can't be blank"},
|
|
||||||
]
|
|
||||||
|
|
||||||
conn = post(new_conn, "/api/reply", %{"content" => "t"})
|
{:badmatch, {:error, data}} = conn = catch_error(post(new_conn, "/api/reply", %{"content" => "t", "thread_id" => 1}))
|
||||||
body = conn |> response(400) |> Poison.decode!
|
assert data == [%{thread_id: "does not exist"}]
|
||||||
assert body["error"]["message"] == [%{"thread_id" => "can't be blank"}]
|
|
||||||
|
|
||||||
conn = post(new_conn, "/api/reply", %{"content" => "t", "thread_id" => 1})
|
|
||||||
body = conn |> response(400) |> Poison.decode!
|
|
||||||
assert body["error"]["message"] == [%{"thread_id" => "does not exist"}]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "insert new reply should succeed" do
|
test "insert new reply should succeed" do
|
||||||
@@ -30,7 +23,7 @@ defmodule MyAppWeb.ReplyControllerTest do
|
|||||||
|> put_req_header("authorization", "Bearer " <> user.token)
|
|> put_req_header("authorization", "Bearer " <> user.token)
|
||||||
|
|
||||||
# insert new thread first
|
# insert new thread first
|
||||||
conn = post(new_conn, "/api/thread", %{"title" => "t", "category_id" => 1, "content" => "t"})
|
conn = post(new_conn, "/api/thread", %{"title" => "t", "category_id" => 1})
|
||||||
body = conn |> response(200) |> Poison.decode!
|
body = conn |> response(200) |> Poison.decode!
|
||||||
|
|
||||||
conn = post(new_conn, "/api/reply", %{"content" => "c", "thread_id" => body["data"]["id"]})
|
conn = post(new_conn, "/api/reply", %{"content" => "c", "thread_id" => body["data"]["id"]})
|
||||||
@@ -41,15 +34,13 @@ defmodule MyAppWeb.ReplyControllerTest do
|
|||||||
|
|
||||||
assert data["content"] == "c"
|
assert data["content"] == "c"
|
||||||
assert data["edited"] == false
|
assert data["edited"] == false
|
||||||
assert data["quote"] == false
|
assert data["quote_id"] == nil
|
||||||
|
|
||||||
# make sure thread reply count and last reply id are updated
|
# make sure thread reply count and last reply id are updated
|
||||||
conn = get(new_conn, "/api/thread?category_id=1")
|
# conn = get(new_conn, "/api/thread?category_id=1")
|
||||||
body = conn |> response(200) |> Poison.decode!
|
# body = conn |> response(200) |> Poison.decode!
|
||||||
|
|
||||||
data = Enum.at(body["data"], 0)
|
# assert Enum.at(body["data"], 0) == "ok"
|
||||||
assert data["reply_count"] == 1
|
|
||||||
assert data["last_reply_id"] == user_id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,29 +7,22 @@ defmodule MyAppWeb.ThreadControllerTest do
|
|||||||
new_conn = build_conn()
|
new_conn = build_conn()
|
||||||
|> put_req_header("authorization", "Bearer " <> user.token)
|
|> put_req_header("authorization", "Bearer " <> user.token)
|
||||||
|
|
||||||
conn = post(new_conn, "/api/thread")
|
{:badmatch, {:error, data}} = catch_error(post(new_conn, "/api/thread"))
|
||||||
body = conn |> response(400) |> Poison.decode!
|
|
||||||
|
|
||||||
assert body["error"]["message"] == [
|
assert data.errors == [
|
||||||
%{"title" => "can't be blank"},
|
title: {"can't be blank", [validation: :required]},
|
||||||
%{"category_id" => "can't be blank"},
|
category_id: {"can't be blank", [validation: :required]},
|
||||||
%{"content" => "can't be blank"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
conn = post(new_conn, "/api/thread", %{"title" => "t"})
|
{:badmatch, {:error, data}} = catch_error(post(new_conn, "/api/thread", %{"title" => "t"}))
|
||||||
body = conn |> response(400) |> Poison.decode!
|
|
||||||
assert body["error"]["message"] == [
|
assert data.errors == [
|
||||||
%{"category_id" => "can't be blank"},
|
category_id: {"can't be blank", [validation: :required]},
|
||||||
%{"content" => "can't be blank"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
conn = post(new_conn, "/api/thread", %{"title" => "t", "category_id" => 1})
|
{:badmatch, {:error, data}} = catch_error(post(new_conn, "/api/thread", %{"title" => "t", "category_id" => 100000}))
|
||||||
body = conn |> response(400) |> Poison.decode!
|
|
||||||
assert body["error"]["message"] == [%{"content" => "can't be blank"}]
|
|
||||||
|
|
||||||
conn = post(new_conn, "/api/thread", %{"title" => "t", "category_id" => 100000, "content" => "t"})
|
assert data.errors == [category_id: {"does not exist", []}]
|
||||||
body = conn |> response(400) |> Poison.decode!
|
|
||||||
assert body["error"]["message"] == [%{"category_id" => "does not exist"}]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "insert new thread should succeed" do
|
test "insert new thread should succeed" do
|
||||||
@@ -40,10 +33,10 @@ defmodule MyAppWeb.ThreadControllerTest do
|
|||||||
body = conn |> response(200) |> Poison.decode!
|
body = conn |> response(200) |> Poison.decode!
|
||||||
|
|
||||||
data = body["data"]
|
data = body["data"]
|
||||||
|
# assert body == "test"
|
||||||
assert data["user_id"] == user.id
|
assert data["user_id"] == user.id
|
||||||
assert data["category_id"] == 1
|
assert data["category_id"] == 1
|
||||||
assert data["title"] == "t"
|
assert data["title"] == "t"
|
||||||
assert data["content"] == "t"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: update thread / delete thread
|
# TODO: update thread / delete thread
|
||||||
|
|||||||
Reference in New Issue
Block a user