rewrite draft using preact
This commit is contained in:
+15
-3
@@ -1,11 +1,23 @@
|
||||
import { h, render } from 'preact';
|
||||
import 'babel-polyfill'; // TODO: remove it
|
||||
import 'mimic'; // TODO: it's for dev only
|
||||
|
||||
import 'common/polyfills'; // TODO: check it
|
||||
|
||||
import { h, render } from 'preact';
|
||||
import Root from './components/root';
|
||||
|
||||
require('./main.scss');
|
||||
import { id } from './common/settings';
|
||||
|
||||
render(<Root />, document.getElementById('remark42'));
|
||||
if (document.readyState !== 'complete') {
|
||||
window.addEventListener('DOMContentLoaded', initApp);
|
||||
} else {
|
||||
initApp();
|
||||
}
|
||||
|
||||
function initApp() {
|
||||
const node = document.getElementById(id);
|
||||
|
||||
if (!node) return;
|
||||
|
||||
render(<Root/>, node.parentElement, node);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import fetcher from './fetcher'
|
||||
|
||||
export const find = ({ url }) => fetcher.get(`/find?url=${url}&sort=time&format=tree`);
|
||||
|
||||
export const vote = ({ id, url, value }) => fetcher.get(`/vote/${id}?url=${url}&vote=${value}`);
|
||||
|
||||
export default {
|
||||
find,
|
||||
vote,
|
||||
};
|
||||
@@ -1,11 +1,15 @@
|
||||
const baseUrl = 'https://demo.remark42.com';
|
||||
const apiBase = '/api/v1'
|
||||
const siteId = 'remark';
|
||||
const nodeId = 'remark42';
|
||||
const id = 'remark42';
|
||||
const url = 'https://radio-t.com/p/2017/12/16/podcast-576/';
|
||||
const userId = 'dev'; // for develop only
|
||||
|
||||
export {
|
||||
module.exports = {
|
||||
baseUrl,
|
||||
siteId,
|
||||
apiBase,
|
||||
nodeId,
|
||||
id,
|
||||
url,
|
||||
userId,
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
$offset: 24px;
|
||||
@@ -0,0 +1,10 @@
|
||||
.comment__avatar {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-right: 8px;
|
||||
border-radius: 4px;
|
||||
background: #eee;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.comment__info {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.comment__score-sign {
|
||||
font-size: 14px;
|
||||
vertical-align: 1px;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.comment__score-value {
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.comment__score {
|
||||
margin-left: 8px;
|
||||
font-size: 0;
|
||||
color: #666;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.comment__text {
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
+ p {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.comment__time {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.comment__username {
|
||||
text-decoration: none;
|
||||
color: #19f;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.comment__vote_selected {
|
||||
border-top-color: #19f;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.comment__vote_type_up {
|
||||
transform: scale(1, -1);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
.comment__vote {
|
||||
display: inline-block;
|
||||
border-top: 9px solid #ccc;
|
||||
border-right: 7px solid transparent;
|
||||
border-left: 7px solid transparent;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-top-color: #19f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@import 'common/variables';
|
||||
|
||||
.comment_level {
|
||||
@for $i from 1 through 5 {
|
||||
&_#{$i} {
|
||||
margin-left: $i * $offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.comment_view_admin {
|
||||
.comment__username {
|
||||
font-weight: 700;
|
||||
color: #ff4700;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { vote } from 'common/api';
|
||||
import { url, userId } from 'common/settings';
|
||||
|
||||
export default class Comment extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { score, votes } = props.data;
|
||||
|
||||
this.state = {
|
||||
score: score,
|
||||
scoreIncreased: userId in votes && votes[userId],
|
||||
scoreDecreased: userId in votes && !votes[userId],
|
||||
};
|
||||
|
||||
this.decreaseScore = this.decreaseScore.bind(this);
|
||||
this.increaseScore = this.increaseScore.bind(this);
|
||||
}
|
||||
|
||||
increaseScore() {
|
||||
const { score, scoreIncreased, scoreDecreased } = this.state;
|
||||
|
||||
if (scoreIncreased) return;
|
||||
|
||||
this.setState({
|
||||
scoreIncreased: !scoreDecreased,
|
||||
scoreDecreased: false,
|
||||
score: score + 1,
|
||||
});
|
||||
|
||||
vote({ id: this.props.data.id, url, value: 1 });
|
||||
}
|
||||
|
||||
decreaseScore() {
|
||||
const { score, scoreIncreased, scoreDecreased } = this.state;
|
||||
|
||||
if (scoreDecreased) return;
|
||||
|
||||
this.setState({
|
||||
scoreDecreased: !scoreIncreased,
|
||||
scoreIncreased: false,
|
||||
score: score - 1,
|
||||
});
|
||||
|
||||
vote({ id: this.props.data.id, url, value: -1 });
|
||||
}
|
||||
|
||||
render(props, { score, scoreIncreased, scoreDecreased }) {
|
||||
const { data, mix, mods = {} } = props;
|
||||
console.log('rerender', data.id)
|
||||
|
||||
const time = new Date(data.time);
|
||||
// TODO: which format for datetime should we choose?
|
||||
// TODO: add smth that will count 'hours ago'
|
||||
// TODO: check out stash's impl
|
||||
const timeStr = `${time.toLocaleDateString()} ${time.toLocaleTimeString()}`;
|
||||
const o = {
|
||||
...data,
|
||||
time: timeStr,
|
||||
score: {
|
||||
value: Math.abs(score),
|
||||
sign: score > 0 ? '+' : (score < 0 ? '−' : ''),
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
// TODO: add default view mod or don't?
|
||||
<div className={b('comment', props, { view: data.user.admin ? 'admin' : null })} data-id={o.id}>
|
||||
<img src={o.user.picture} alt="" className="comment__avatar"/>
|
||||
|
||||
<div className="comment__content">
|
||||
<div className="comment__info">
|
||||
<a href="#" className="comment__username">{o.user.name}</a>
|
||||
|
||||
<span className="comment__score">
|
||||
<span
|
||||
className={b('comment__vote', {}, { type: 'up', selected: scoreIncreased })}
|
||||
onClick={this.increaseScore}
|
||||
>vote up</span>
|
||||
<span className="comment__score-sign">{o.score.sign}</span>
|
||||
<span className="comment__score-value">{o.score.value}</span>
|
||||
<span
|
||||
className={b('comment__vote', {}, { type: 'down', selected: scoreDecreased })}
|
||||
onClick={this.decreaseScore}
|
||||
>vote down</span>
|
||||
</span>
|
||||
|
||||
<span className="comment__time">{o.time}</span>
|
||||
</div>
|
||||
|
||||
<div className="comment__text" dangerouslySetInnerHTML={{ __html: o.text }}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
@import 'common/variables';
|
||||
|
||||
.comment {
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: $offset;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export { default } from './comment';
|
||||
|
||||
require('./comment.scss');
|
||||
|
||||
require('./__avatar/comment__avatar.scss');
|
||||
require('./__info/comment__info.scss');
|
||||
require('./__score/comment__score.scss');
|
||||
require('./__score-sign/comment__score-sign.scss');
|
||||
require('./__score-value/comment__score-value.scss');
|
||||
require('./__text/comment__text.scss');
|
||||
require('./__time/comment__time.scss');
|
||||
require('./__username/comment__username.scss');
|
||||
|
||||
require('./__vote/comment__vote.scss');
|
||||
require('./__vote/_selected/comment__vote_selected.scss');
|
||||
require('./__vote/_type/_up/comment__vote_type_up.scss');
|
||||
|
||||
require('./_level/comment_level.scss');
|
||||
|
||||
require('./_view/_admin/comment_view_admin.scss');
|
||||
@@ -1 +1,3 @@
|
||||
export { default } from './root';
|
||||
|
||||
require('./root.scss');
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { Component } from 'preact';
|
||||
import fetcher from 'common/fetcher';
|
||||
import { h, Component } from 'preact';
|
||||
import api from 'common/api';
|
||||
|
||||
import { url, id } from 'common/settings';
|
||||
|
||||
import Thread from 'components/thread';
|
||||
|
||||
export default class Root extends Component {
|
||||
componentDidMount() {
|
||||
// TODO: add preloader
|
||||
// TODO: all of these settings must be optional params
|
||||
fetcher
|
||||
.get('/find?url=https://radio-t.com/p/2017/12/16/podcast-576/&sort=time&format=tree')
|
||||
.then(this.loadData.bind(this));
|
||||
api.find({ url }).then(({ comments } = {}) => this.setState({ comments }));
|
||||
}
|
||||
|
||||
loadData(data) {
|
||||
this.setState({ data: JSON.stringify(data) })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data } = this.state;
|
||||
|
||||
return data;
|
||||
render({}, { comments = [] }) {
|
||||
return (
|
||||
<div className="root" id={id}>
|
||||
{
|
||||
comments.map(thread => <Thread data={thread} mods={{ level: 0 }}/>)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
@import 'common/variables';
|
||||
|
||||
.root {
|
||||
margin-bottom: -$offset;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default } from './thread';
|
||||
|
||||
require('./thread.scss');
|
||||
@@ -0,0 +1,24 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import Comment from 'components/comment';
|
||||
|
||||
export default class Thread extends Component {
|
||||
render(props, state) {
|
||||
const { data: { comment, replies = [] }, mix, mods = {} } = props;
|
||||
|
||||
return (
|
||||
<div className={b('thread', props)}>
|
||||
<Comment data={comment} mods={{ level: mods.level }}/>
|
||||
|
||||
{
|
||||
!!replies.length && replies.map(thread => (
|
||||
<Thread
|
||||
data={thread}
|
||||
mods={{ level: mods.level < 5 ? mods.level + 1 : mods.level }}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@import 'common/variables';
|
||||
|
||||
.thread {
|
||||
margin-bottom: $offset;
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
.remark42 {
|
||||
&__comment {
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
|
||||
+ .remark42__comment {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&_level {
|
||||
@for $i from 1 through 5 {
|
||||
&_#{$i} {
|
||||
margin-left: $i * 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&_view_admin {
|
||||
.remark42__username {
|
||||
font-weight: 700;
|
||||
color: #ff4700;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
+ p {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__username {
|
||||
text-decoration: none;
|
||||
color: #19f;
|
||||
}
|
||||
|
||||
&__score {
|
||||
margin-left: 8px;
|
||||
font-size: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&__score-value {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__score-sign {
|
||||
font-size: 14px;
|
||||
vertical-align: 1px;
|
||||
}
|
||||
|
||||
&__time {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&__vote {
|
||||
display: inline-block;
|
||||
border-top: 9px solid #ccc;
|
||||
border-right: 7px solid transparent;
|
||||
border-left: 7px solid transparent;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
|
||||
// TODO: add mod for selected
|
||||
|
||||
&:hover {
|
||||
border-top-color: #19f;
|
||||
}
|
||||
|
||||
&_type_up {
|
||||
transform: scale(1, -1);
|
||||
}
|
||||
|
||||
&_type_down {
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-right: 8px;
|
||||
border-radius: 4px;
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import { nodeId } from './common/settings';
|
||||
|
||||
import fetcher from './common/fetcher';
|
||||
|
||||
let node = null;
|
||||
let afterInit = null;
|
||||
|
||||
if (document.readyState !== 'complete') {
|
||||
window.addEventListener('DOMContentLoaded', initNode);
|
||||
} else {
|
||||
initNode();
|
||||
}
|
||||
|
||||
function initNode () {
|
||||
if (node) return;
|
||||
|
||||
node = document.getElementById(nodeId);
|
||||
|
||||
if (!node) return;
|
||||
|
||||
if (afterInit) {
|
||||
afterInit();
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(rootNode, e) {
|
||||
if (!e.target) return;
|
||||
|
||||
if (e.target && e.target.classList.contains('remark42__vote')) {
|
||||
handleVote(rootNode, e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleVote(rootNode, e) {
|
||||
const { target: node } = e;
|
||||
|
||||
if (node.classList.contains('remark42__vote_selected')) return;
|
||||
|
||||
const commentId = node.closest('.remark42__comment').dataset.id;
|
||||
|
||||
if (node.classList.contains('remark42__vote_type_up')) {
|
||||
console.log('+1');
|
||||
changeScore(rootNode, 1);
|
||||
fetcher.put(`/vote/${commentId}?url=https://radio-t.com/p/2017/12/16/podcast-576/&vote=1`);
|
||||
} else {
|
||||
console.log('-1');
|
||||
changeScore(rootNode, -1);
|
||||
fetcher.put(`/vote/${commentId}?url=https://radio-t.com/p/2017/12/16/podcast-576/&vote=-1`);
|
||||
}
|
||||
|
||||
node.classList.add('remark42__vote_selected');
|
||||
}
|
||||
|
||||
function changeScore(rootNode, delta) {
|
||||
const valueNode = rootNode.querySelector('.remark42__score-value');
|
||||
const currentValue = parseInt(valueNode.dataset.value);
|
||||
const newValue = currentValue + delta;
|
||||
|
||||
valueNode.dataset.value = newValue;
|
||||
valueNode.textContent = Math.abs(newValue);
|
||||
}
|
||||
|
||||
export default data => {
|
||||
// TODO: link to profile?
|
||||
// TODO: link to comment?
|
||||
// TODO: add photo?
|
||||
const templateComment = `
|
||||
<div
|
||||
class="remark42__comment remark42__comment_level_{%= o.mods.level %} {%= o.mods.view ? ('remark42__comment_view_' + o.mods.view) : '' %}"
|
||||
data-id="{%= o.id %}">
|
||||
<img src="{%= o.user.picture %}" alt="" class="remark42__avatar">
|
||||
|
||||
<div class="remark42__content">
|
||||
<div class="remark42__info">
|
||||
<a href="#" class="remark42__username">{%= o.user.name %}</a>
|
||||
|
||||
<span class="remark42__score">
|
||||
<a href="#" class="remark42__vote remark42__vote_type_up">vote up</a>
|
||||
<span class="remark42__score-sign">{%= o.score.sign %}</span>
|
||||
<span class="remark42__score-value" data-value="{%= o.score.rawValue %}">{%= o.score.value %}</span>
|
||||
<a href="#" class="remark42__vote remark42__vote_type_down">vote down</a>
|
||||
</span>
|
||||
|
||||
<span class="remark42__time">{%= o.time %}</span>
|
||||
</div>
|
||||
|
||||
<div class="remark42__text">
|
||||
{%# o.text %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const renderComment = ({ comment, level }) => {
|
||||
const time = new Date(comment.time);
|
||||
// TODO: which format for datetime should we choose?
|
||||
// TODO: add smth that will count 'hours ago'
|
||||
// TODO: check out stash's impl
|
||||
const timeStr = `${time.toLocaleDateString()} ${time.toLocaleTimeString()}`;
|
||||
const data = {
|
||||
...comment,
|
||||
time: timeStr,
|
||||
score: {
|
||||
rawValue: comment.score,
|
||||
value: Math.abs(comment.score),
|
||||
sign: comment.score > 0 ? '+' : (comment.score < 0 ? '−' : ''),
|
||||
},
|
||||
mods: {
|
||||
level: level > 5 ? 5 : level,
|
||||
view: comment.user.admin ? 'admin' : '', // TODO: add default view or don't?
|
||||
},
|
||||
};
|
||||
|
||||
return tmpl(templateComment, data);
|
||||
}
|
||||
|
||||
const renderThread = ({ comment, replies, level }) => {
|
||||
let result = [renderComment({ comment, level })];
|
||||
|
||||
if (replies) {
|
||||
result = result.concat(replies.map(thread => renderThread({ ...thread, level: level + 1 })));
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
const result = data.comments.reduce((acc, thread) => acc.concat(renderThread({ ...thread, level: 0 })), []).join('');
|
||||
|
||||
node.className = 'remark42';
|
||||
node.innerHTML = result;
|
||||
|
||||
node.addEventListener('click', handleClick.bind(null, node));
|
||||
};
|
||||
|
||||
if (node) {
|
||||
render();
|
||||
} else {
|
||||
afterInit = render;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user