diff --git a/web/app/app.js b/web/app/app.js index 8a78288c..f44116e7 100644 --- a/web/app/app.js +++ b/web/app/app.js @@ -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(, document.getElementById('remark42')); +if (document.readyState !== 'complete') { + window.addEventListener('DOMContentLoaded', initApp); +} else { + initApp(); +} + +function initApp() { + const node = document.getElementById(id); + + if (!node) return; + + render(, node.parentElement, node); +} diff --git a/web/app/common/api.js b/web/app/common/api.js new file mode 100644 index 00000000..0b357cab --- /dev/null +++ b/web/app/common/api.js @@ -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, +}; diff --git a/web/app/common/settings.js b/web/app/common/settings.js index a738dcb5..4f521f23 100644 --- a/web/app/common/settings.js +++ b/web/app/common/settings.js @@ -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, }; diff --git a/web/app/common/variables.scss b/web/app/common/variables.scss new file mode 100644 index 00000000..c85aee9f --- /dev/null +++ b/web/app/common/variables.scss @@ -0,0 +1 @@ +$offset: 24px; diff --git a/web/app/components/comment/__avatar/comment__avatar.scss b/web/app/components/comment/__avatar/comment__avatar.scss new file mode 100644 index 00000000..c70903c1 --- /dev/null +++ b/web/app/components/comment/__avatar/comment__avatar.scss @@ -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; +} diff --git a/web/app/components/comment/__info/comment__info.scss b/web/app/components/comment/__info/comment__info.scss new file mode 100644 index 00000000..92fa999d --- /dev/null +++ b/web/app/components/comment/__info/comment__info.scss @@ -0,0 +1,3 @@ +.comment__info { + margin-bottom: 4px; +} diff --git a/web/app/components/comment/__score-sign/comment__score-sign.scss b/web/app/components/comment/__score-sign/comment__score-sign.scss new file mode 100644 index 00000000..e4bf6c80 --- /dev/null +++ b/web/app/components/comment/__score-sign/comment__score-sign.scss @@ -0,0 +1,4 @@ +.comment__score-sign { + font-size: 14px; + vertical-align: 1px; +} diff --git a/web/app/components/comment/__score-value/comment__score-value.scss b/web/app/components/comment/__score-value/comment__score-value.scss new file mode 100644 index 00000000..6f2c9ac9 --- /dev/null +++ b/web/app/components/comment/__score-value/comment__score-value.scss @@ -0,0 +1,3 @@ +.comment__score-value { + font-size: 14px; +} diff --git a/web/app/components/comment/__score/comment__score.scss b/web/app/components/comment/__score/comment__score.scss new file mode 100644 index 00000000..a5bb33b0 --- /dev/null +++ b/web/app/components/comment/__score/comment__score.scss @@ -0,0 +1,6 @@ +.comment__score { + margin-left: 8px; + font-size: 0; + color: #666; + cursor: default; +} diff --git a/web/app/components/comment/__text/comment__text.scss b/web/app/components/comment/__text/comment__text.scss new file mode 100644 index 00000000..550cfe0c --- /dev/null +++ b/web/app/components/comment/__text/comment__text.scss @@ -0,0 +1,9 @@ +.comment__text { + p { + margin: 0; + + + p { + margin-top: 5px; + } + } +} diff --git a/web/app/components/comment/__time/comment__time.scss b/web/app/components/comment/__time/comment__time.scss new file mode 100644 index 00000000..5c6f0910 --- /dev/null +++ b/web/app/components/comment/__time/comment__time.scss @@ -0,0 +1,5 @@ +.comment__time { + margin-left: 8px; + font-size: 14px; + color: #666; +} diff --git a/web/app/components/comment/__username/comment__username.scss b/web/app/components/comment/__username/comment__username.scss new file mode 100644 index 00000000..8b641b32 --- /dev/null +++ b/web/app/components/comment/__username/comment__username.scss @@ -0,0 +1,4 @@ +.comment__username { + text-decoration: none; + color: #19f; +} diff --git a/web/app/components/comment/__vote/_selected/comment__vote_selected.scss b/web/app/components/comment/__vote/_selected/comment__vote_selected.scss new file mode 100644 index 00000000..dd261a87 --- /dev/null +++ b/web/app/components/comment/__vote/_selected/comment__vote_selected.scss @@ -0,0 +1,4 @@ +.comment__vote_selected { + border-top-color: #19f; + cursor: default; +} diff --git a/web/app/components/comment/__vote/_type/_up/comment__vote_type_up.scss b/web/app/components/comment/__vote/_type/_up/comment__vote_type_up.scss new file mode 100644 index 00000000..4c296261 --- /dev/null +++ b/web/app/components/comment/__vote/_type/_up/comment__vote_type_up.scss @@ -0,0 +1,3 @@ +.comment__vote_type_up { + transform: scale(1, -1); +} diff --git a/web/app/components/comment/__vote/comment__vote.scss b/web/app/components/comment/__vote/comment__vote.scss new file mode 100644 index 00000000..75e663a5 --- /dev/null +++ b/web/app/components/comment/__vote/comment__vote.scss @@ -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; + } +} diff --git a/web/app/components/comment/_level/comment_level.scss b/web/app/components/comment/_level/comment_level.scss new file mode 100644 index 00000000..cd65213a --- /dev/null +++ b/web/app/components/comment/_level/comment_level.scss @@ -0,0 +1,9 @@ +@import 'common/variables'; + +.comment_level { + @for $i from 1 through 5 { + &_#{$i} { + margin-left: $i * $offset; + } + } +} diff --git a/web/app/components/comment/_view/_admin/comment_view_admin.scss b/web/app/components/comment/_view/_admin/comment_view_admin.scss new file mode 100644 index 00000000..5ab3a17c --- /dev/null +++ b/web/app/components/comment/_view/_admin/comment_view_admin.scss @@ -0,0 +1,6 @@ +.comment_view_admin { + .comment__username { + font-weight: 700; + color: #ff4700; + }; +} diff --git a/web/app/components/comment/comment.jsx b/web/app/components/comment/comment.jsx new file mode 100644 index 00000000..e4225829 --- /dev/null +++ b/web/app/components/comment/comment.jsx @@ -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? +
+ + +
+
+ {o.user.name} + + + vote up + {o.score.sign} + {o.score.value} + vote down + + + {o.time} +
+ +
+
+
+ ); + } +} diff --git a/web/app/components/comment/comment.scss b/web/app/components/comment/comment.scss new file mode 100644 index 00000000..e049067f --- /dev/null +++ b/web/app/components/comment/comment.scss @@ -0,0 +1,10 @@ +@import 'common/variables'; + +.comment { + display: flex; + position: relative; + overflow: hidden; + margin-bottom: $offset; + font-size: 16px; + line-height: 20px; +} diff --git a/web/app/components/comment/index.js b/web/app/components/comment/index.js new file mode 100644 index 00000000..883c12cd --- /dev/null +++ b/web/app/components/comment/index.js @@ -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'); diff --git a/web/app/components/root/index.js b/web/app/components/root/index.js index 4f4ec54a..15a696f0 100644 --- a/web/app/components/root/index.js +++ b/web/app/components/root/index.js @@ -1 +1,3 @@ export { default } from './root'; + +require('./root.scss'); diff --git a/web/app/components/root/root.jsx b/web/app/components/root/root.jsx index b1189aaf..fa4e79e7 100644 --- a/web/app/components/root/root.jsx +++ b/web/app/components/root/root.jsx @@ -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 ( +
+ { + comments.map(thread => ) + } +
+ ); } } diff --git a/web/app/components/root/root.scss b/web/app/components/root/root.scss new file mode 100644 index 00000000..db297aeb --- /dev/null +++ b/web/app/components/root/root.scss @@ -0,0 +1,5 @@ +@import 'common/variables'; + +.root { + margin-bottom: -$offset; +} diff --git a/web/app/components/thread/index.js b/web/app/components/thread/index.js new file mode 100644 index 00000000..c774686b --- /dev/null +++ b/web/app/components/thread/index.js @@ -0,0 +1,3 @@ +export { default } from './thread'; + +require('./thread.scss'); diff --git a/web/app/components/thread/thread.jsx b/web/app/components/thread/thread.jsx new file mode 100644 index 00000000..ba3b79e7 --- /dev/null +++ b/web/app/components/thread/thread.jsx @@ -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 ( +
+ + + { + !!replies.length && replies.map(thread => ( + + )) + } +
+ ); + } +} diff --git a/web/app/components/thread/thread.scss b/web/app/components/thread/thread.scss new file mode 100644 index 00000000..59307364 --- /dev/null +++ b/web/app/components/thread/thread.scss @@ -0,0 +1,5 @@ +@import 'common/variables'; + +.thread { + margin-bottom: $offset; +} diff --git a/web/app/main.scss b/web/app/main.scss deleted file mode 100644 index b2d46487..00000000 --- a/web/app/main.scss +++ /dev/null @@ -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; - } -} diff --git a/web/app/render.js b/web/app/render.js deleted file mode 100644 index 01021a30..00000000 --- a/web/app/render.js +++ /dev/null @@ -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 = ` -
- - -
-
- {%= o.user.name %} - - - vote up - {%= o.score.sign %} - {%= o.score.value %} - vote down - - - {%= o.time %} -
- -
- {%# o.text %} -
-
-
- `; - - 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; - } -} diff --git a/web/package-lock.json b/web/package-lock.json index f5a3c2fa..6f194f26 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/package.json b/web/package.json index 97723c47..431d01fa 100644 --- a/web/package.json +++ b/web/package.json @@ -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" diff --git a/web/webpack.config.js b/web/webpack.config.js index 2077eee3..b2a48892 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -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') } }