rewrite draft using preact

This commit is contained in:
igoradamenko
2018-01-23 20:15:35 +02:00
parent 26e2445913
commit 1f4e1b160f
31 changed files with 374 additions and 266 deletions
+15 -3
View File
@@ -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);
}
+10
View File
@@ -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,
};
+7 -3
View File
@@ -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,
};
+1
View File
@@ -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;
};
}
+98
View File
@@ -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>
);
}
}
+10
View File
@@ -0,0 +1,10 @@
@import 'common/variables';
.comment {
display: flex;
position: relative;
overflow: hidden;
margin-bottom: $offset;
font-size: 16px;
line-height: 20px;
}
+20
View File
@@ -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');
+2
View File
@@ -1 +1,3 @@
export { default } from './root';
require('./root.scss');
+15 -14
View File
@@ -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>
);
}
}
+5
View File
@@ -0,0 +1,5 @@
@import 'common/variables';
.root {
margin-bottom: -$offset;
}
+3
View File
@@ -0,0 +1,3 @@
export { default } from './thread';
require('./thread.scss');
+24
View File
@@ -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>
);
}
}
+5
View File
@@ -0,0 +1,5 @@
@import 'common/variables';
.thread {
margin-bottom: $offset;
}
-101
View File
@@ -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;
}
}
-141
View File
@@ -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;
}
}
+72
View File
@@ -7765,6 +7765,78 @@
"integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=",
"dev": true
},
"postcss-wrap": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/postcss-wrap/-/postcss-wrap-0.0.4.tgz",
"integrity": "sha1-Eye9caqki+mdoVrG5Bs1X3mJie4=",
"dev": true,
"requires": {
"colors": "1.1.2",
"postcss": "5.2.18"
},
"dependencies": {
"ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
"dev": true
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
"ansi-styles": "2.2.1",
"escape-string-regexp": "1.0.5",
"has-ansi": "2.0.0",
"strip-ansi": "3.0.1",
"supports-color": "2.0.0"
},
"dependencies": {
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
"dev": true
}
}
},
"has-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
"dev": true
},
"postcss": {
"version": "5.2.18",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
"dev": true,
"requires": {
"chalk": "1.1.3",
"js-base64": "2.4.0",
"source-map": "0.5.7",
"supports-color": "3.2.3"
}
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
},
"supports-color": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
"integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
"dev": true,
"requires": {
"has-flag": "1.0.0"
}
}
}
},
"postcss-zindex": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz",
+2 -1
View File
@@ -23,7 +23,8 @@
"postcss-csso": "^2.0.0",
"postcss-loader": "^2.0.5",
"postcss-url": "^6.1.0",
"sass-loader": "^6.0.4",
"postcss-wrap": "0.0.4",
"sass-loader": "^6.0.6",
"style-loader": "^0.19.1",
"webpack": "^3.3.0",
"webpack-dev-server": "^2.7.1"
+6 -3
View File
@@ -8,6 +8,7 @@ const Html = require('html-webpack-plugin');
const Provide = webpack.ProvidePlugin;
const Define = webpack.DefinePlugin;
const { id } = require('./app/common/settings');
const publicFolder = path.resolve(__dirname, 'public');
const env = process.env.NODE_ENV || 'dev';
const hash = env === 'production' ? '.[chunkhash]' : '.[hash]';
@@ -54,15 +55,17 @@ module.exports = {
loader: 'postcss-loader',
options: {
plugins: [
require('autoprefixer')({browsers: ['> 1%']}),
require('postcss-url')({url: 'inline', maxSize: 5}),
require('postcss-csso')
require('autoprefixer')({ browsers: ['> 1%'] }),
require('postcss-url')({ url: 'inline', maxSize: 5 }),
require('postcss-wrap')({ selector: `#${id}` }),
require('postcss-csso'),
]
}
},
{
loader: 'sass-loader',
options: {
includePaths: [path.resolve(__dirname, 'app')],
// data: fs.readFileSync(path.resolve(__dirname, 'common/vars/vars.scss'), 'utf-8')
}
}