mirror of
https://github.com/mgerb/classic-wow-forums
synced 2026-01-10 17:12:48 +00:00
client - block quote - adjusted markdown editor styling
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
.editor-container {
|
.editor-background {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -6,43 +6,48 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background-color: rgba(0, 0, 0, .9);
|
background-color: rgba(0, 0, 0, .9);
|
||||||
padding-top: 50px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 800px;
|
||||||
|
max-width: 1000px;
|
||||||
|
padding: 20px 40px 40px;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
height: 80%;
|
overflow-y: auto;
|
||||||
display: flex;
|
height: 100%;
|
||||||
flex-direction: column;
|
margin-bottom: 0;
|
||||||
padding-bottom: 40px;
|
|
||||||
|
|
||||||
&__character-count {
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
height: 25px;
|
height: 25px;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__text-area {
|
&__text-area {
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
resize: none;
|
max-height: 300px;
|
||||||
|
max-width: 100%;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__preview {
|
&__preview {
|
||||||
min-height: 100px;
|
padding: 10px;
|
||||||
border-radius: 2px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__submit {
|
&__submit {
|
||||||
padding: 20px 0;
|
padding: 10px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__error-message {
|
&__error-message {
|
||||||
margin-left: 10px;
|
margin-bottom: 10px;
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import marked from 'marked';
|
import marked from 'marked';
|
||||||
|
import { get } from 'lodash';
|
||||||
import axios from '../../axios/axios';
|
import axios from '../../axios/axios';
|
||||||
import { ContentContainer } from '../content-container/content-container';
|
import { ContentContainer } from '../content-container/content-container';
|
||||||
|
import { ReplyModel } from '../../model';
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
onClose: (cancel: boolean) => any;
|
onClose: (cancel: boolean) => any;
|
||||||
|
quotedReply?: ReplyModel;
|
||||||
threadId?: string;
|
threadId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,11 +18,14 @@ interface State {
|
|||||||
content: string;
|
content: string;
|
||||||
contentPreview: string;
|
contentPreview: string;
|
||||||
contentCharacterCount: number;
|
contentCharacterCount: number;
|
||||||
titleCharacterCount: number;
|
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Editor extends React.Component<Props, State> {
|
export class Editor extends React.Component<Props, State> {
|
||||||
|
|
||||||
|
private titleRef: any;
|
||||||
|
private contentRef: any;
|
||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -27,10 +33,24 @@ export class Editor extends React.Component<Props, State> {
|
|||||||
content: '',
|
content: '',
|
||||||
contentPreview: '',
|
contentPreview: '',
|
||||||
contentCharacterCount: 0,
|
contentCharacterCount: 0,
|
||||||
titleCharacterCount: 0,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// disable scrolling in the background
|
||||||
|
componentDidMount() {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
if (this.titleRef) {
|
||||||
|
this.titleRef.focus();
|
||||||
|
} else {
|
||||||
|
this.contentRef.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.body.style.removeProperty('overflow');
|
||||||
|
}
|
||||||
|
|
||||||
onContentChange(event: any) {
|
onContentChange(event: any) {
|
||||||
this.setState({
|
this.setState({
|
||||||
content: event.target.value,
|
content: event.target.value,
|
||||||
@@ -42,7 +62,6 @@ export class Editor extends React.Component<Props, State> {
|
|||||||
onTitleChange(event: any) {
|
onTitleChange(event: any) {
|
||||||
this.setState({
|
this.setState({
|
||||||
title: event.target.value,
|
title: event.target.value,
|
||||||
titleCharacterCount: event.target.value.length,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +85,7 @@ export class Editor extends React.Component<Props, State> {
|
|||||||
const data = {
|
const data = {
|
||||||
content,
|
content,
|
||||||
thread_id: this.props.threadId,
|
thread_id: this.props.threadId,
|
||||||
|
quote_id: get(this.props, 'quotedReply.id') || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -98,46 +118,61 @@ export class Editor extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderThreadPortion() {
|
private renderTopicInput(): any {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex--column">
|
<div>
|
||||||
<h2 style={{ color: 'white' }}>New Topic</h2>
|
<div><label>Title</label></div>
|
||||||
<label>Title</label>
|
|
||||||
<input type="text"
|
<input type="text"
|
||||||
|
ref={ref => this.titleRef = ref}
|
||||||
className="input editor__title"
|
className="input editor__title"
|
||||||
onChange={event => this.onTitleChange(event)}
|
onChange={event => this.onTitleChange(event)}
|
||||||
maxLength={300}/>
|
maxLength={300}/>
|
||||||
<div className="editor__character-count">{this.state.contentCharacterCount}/2000</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: quote
|
renderQuotedReply() {
|
||||||
renderReplyPortion() {
|
return (this.props.quotedReply &&
|
||||||
return (
|
<blockquote className="blockquote">
|
||||||
<h2 style={{ color: 'white' }}>New Reply</h2>
|
<small>Q u o t e:</small>
|
||||||
|
<small dangerouslySetInnerHTML={{ __html: marked(this.props.quotedReply!.content, { sanitize: true }) }}/>
|
||||||
|
</blockquote>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="editor-container">
|
<div className="editor-background">
|
||||||
<form onSubmit={event => this.onSubmit(event)} onReset={() => this.props.onClose(true)}>
|
<ContentContainer className="editor-container">
|
||||||
<ContentContainer className="editor">
|
<form className="editor" onSubmit={event => this.onSubmit(event)} onReset={() => this.props.onClose(true)}>
|
||||||
{this.props.threadId ? this.renderReplyPortion() : this.renderThreadPortion()}
|
|
||||||
<label>Content</label>
|
<h2 style={{ color: 'white' }}>New {this.props.threadId ? 'Reply' : 'Topic'}</h2>
|
||||||
<textarea className="input editor__text-area flex-1" onChange={event => this.onContentChange(event)} maxLength={2000}/>
|
{!this.props.threadId && this.renderTopicInput()}
|
||||||
<div className="editor__character-count">{this.state.contentCharacterCount}/2000</div>
|
|
||||||
<label>Preview</label>
|
<div><label>Content</label></div>
|
||||||
<div className="editor__preview flex-1" dangerouslySetInnerHTML={{ __html: this.state.contentPreview }}></div>
|
<textarea className="input editor__text-area flex-1"
|
||||||
|
onChange={event => this.onContentChange(event)} maxLength={2000}
|
||||||
|
ref={ref => this.contentRef = ref}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="editor__submit">
|
<div className="editor__submit">
|
||||||
<input type="submit" value="Submit" className="input__button"/>
|
<div>
|
||||||
<input type="reset" value="Cancel" className="input__button" style={{ marginLeft: '10px' }}/>
|
<input type="submit" value="Submit" className="input__button"/>
|
||||||
<span className="editor__error-message">{this.state.errorMessage}</span>
|
<input type="reset" value="Cancel" className="input__button" style={{ marginLeft: '10px' }}/>
|
||||||
|
</div>
|
||||||
|
<div>{this.state.contentCharacterCount}/2000</div>
|
||||||
</div>
|
</div>
|
||||||
</ContentContainer>
|
|
||||||
</form>
|
<div className="editor__error-message">{this.state.errorMessage}</div>
|
||||||
|
|
||||||
|
<div><label>Preview</label></div>
|
||||||
|
<div className="markdown-container">
|
||||||
|
{this.renderQuotedReply()}
|
||||||
|
<div className="editor__preview" dangerouslySetInnerHTML={{ __html: this.state.contentPreview }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</ContentContainer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface ReplyModel {
|
|||||||
edited: boolean;
|
edited: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
inserted_at: string;
|
inserted_at: string;
|
||||||
quote: boolean;
|
quote_id: number;
|
||||||
thread_id: number;
|
thread_id: number;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
|
|||||||
@@ -81,8 +81,6 @@
|
|||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { get, map } from 'lodash';
|
import { get, find, map } from 'lodash';
|
||||||
import marked from 'marked';
|
import marked from 'marked';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { CharacterService, ThreadService } from '../../services';
|
import { CharacterService, ThreadService } from '../../services';
|
||||||
@@ -11,6 +11,7 @@ import './thread.scss';
|
|||||||
interface Props extends RouteComponentProps<any> {}
|
interface Props extends RouteComponentProps<any> {}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
quotedReply?: ReplyModel;
|
||||||
showEditor: boolean;
|
showEditor: boolean;
|
||||||
thread?: ThreadModel;
|
thread?: ThreadModel;
|
||||||
}
|
}
|
||||||
@@ -31,7 +32,6 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
private async getReplies() {
|
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.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 });
|
|
||||||
return reply;
|
return reply;
|
||||||
});
|
});
|
||||||
this.setState({ thread });
|
this.setState({ thread });
|
||||||
@@ -42,11 +42,17 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onQuoteClick(reply: ReplyModel) {
|
private onQuoteClick(reply: ReplyModel) {
|
||||||
console.log(reply);
|
this.setState({
|
||||||
|
showEditor: true,
|
||||||
|
quotedReply: reply,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onEditorClose(cancel: boolean) {
|
private onEditorClose(cancel: boolean) {
|
||||||
this.setState({ showEditor: false });
|
this.setState({
|
||||||
|
showEditor: false,
|
||||||
|
quotedReply: undefined,
|
||||||
|
});
|
||||||
if (!cancel) {
|
if (!cancel) {
|
||||||
this.getReplies();
|
this.getReplies();
|
||||||
}
|
}
|
||||||
@@ -67,6 +73,16 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderQuotedReply(reply: ReplyModel) {
|
||||||
|
const quotedReply = find(this.state.thread!.replies, { id: reply.quote_id });
|
||||||
|
return (quotedReply &&
|
||||||
|
<blockquote className="blockquote">
|
||||||
|
<small>Q u o t e:</small>
|
||||||
|
<small dangerouslySetInnerHTML={{ __html: marked(quotedReply.content, { sanitize: true }) }}/>
|
||||||
|
</blockquote>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderUserInfo(reply: ReplyModel) {
|
renderUserInfo(reply: ReplyModel) {
|
||||||
const { battletag, character_avatar, character_class, character_guild, character_name, character_realm } = reply.user;
|
const { battletag, character_avatar, character_class, character_guild, character_name, character_realm } = reply.user;
|
||||||
return (
|
return (
|
||||||
@@ -90,6 +106,7 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
<div className={`reply ${replyDark}`}>
|
<div className={`reply ${replyDark}`}>
|
||||||
{this.renderUserInfo(reply)}
|
{this.renderUserInfo(reply)}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
||||||
<div className="reply__title">
|
<div className="reply__title">
|
||||||
<div>
|
<div>
|
||||||
<b>{`${index + 1}. `}{index > 0 && 'Re: '}{this.state.thread!.title}</b>
|
<b>{`${index + 1}. `}{index > 0 && 'Re: '}{this.state.thread!.title}</b>
|
||||||
@@ -104,7 +121,12 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
onClick={() => this.onReplyClick()}/>
|
onClick={() => this.onReplyClick()}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="reply__content" dangerouslySetInnerHTML={{ __html: reply.content }}/>
|
|
||||||
|
<div className="reply__content markdown-container">
|
||||||
|
{this.renderQuotedReply(reply)}
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: marked(reply.content, { sanitize: true }) }}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +144,12 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollToTop {...this.props}>
|
<ScrollToTop {...this.props}>
|
||||||
{this.state.showEditor && <Editor threadId={this.props.match.params['threadId']} onClose={cancel => this.onEditorClose(cancel)}/>}
|
{this.state.showEditor &&
|
||||||
|
<Editor threadId={this.props.match.params['threadId']}
|
||||||
|
onClose={cancel => this.onEditorClose(cancel)}
|
||||||
|
quotedReply={this.state.quotedReply}
|
||||||
|
/>
|
||||||
|
}
|
||||||
<div className="topic-bg">
|
<div className="topic-bg">
|
||||||
<div className="threadTopic-container">
|
<div className="threadTopic-container">
|
||||||
<div className="threadTopic">
|
<div className="threadTopic">
|
||||||
|
|||||||
@@ -123,9 +123,37 @@ span.grey {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 9pt;
|
font-size: 9pt;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blockquote {
|
||||||
|
margin: 14px 40px 40px;
|
||||||
|
padding: 5px;
|
||||||
|
color: #ffffff;
|
||||||
|
border-top: 1px solid grey;
|
||||||
|
border-bottom: 1px solid grey;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
small :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-container {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user