add draft of web app
This commit is contained in:
@@ -7,3 +7,6 @@ target
|
||||
debug
|
||||
debug.test
|
||||
.vscode
|
||||
.idea/
|
||||
/web/node_modules/
|
||||
/web/public/
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'mimic'; // TODO: it's for dev only
|
||||
import fetcher from './fetcher';
|
||||
import render from './render';
|
||||
|
||||
require('./main.scss');
|
||||
|
||||
// TODO: add preloader
|
||||
// TODO: all of these settings must be optional params
|
||||
fetcher
|
||||
.get('/find?site=remark&url=https://radio-t.com/p/2017/12/16/podcast-576/&sort=time&format=tree')
|
||||
.then(render);
|
||||
@@ -0,0 +1,62 @@
|
||||
import './promises';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { baseUrl, apiBase, siteId } from './settings';
|
||||
|
||||
const fetcher = {};
|
||||
const methods = ['get', 'post', 'put', 'patch', 'delete', 'head'];
|
||||
const basename = `${baseUrl}${apiBase}`;
|
||||
|
||||
const { CancelToken } = axios;
|
||||
let cancelHandler = [];
|
||||
|
||||
fetcher.cancel = (mask) => {
|
||||
cancelHandler.forEach(req => {
|
||||
if (req.url.includes(mask)) {
|
||||
req.executor('Operation canceled by the user.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
methods.forEach(method => {
|
||||
fetcher[method] = (url, body = {}, heads) => new Promise((resolve, reject) => {
|
||||
const headers = Object.assign({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}, heads);
|
||||
|
||||
const parameters = {
|
||||
method,
|
||||
headers,
|
||||
};
|
||||
|
||||
// тут передаются данные не только в виде обычного js объекта но и в виде
|
||||
// объекта formData у которого Object.keys(formData).length === 0
|
||||
// if (Object.keys(body).length || body.toString().includes('FormData')) {
|
||||
// parameters.data = body;
|
||||
// }
|
||||
|
||||
parameters.url = `${basename}${url}`;
|
||||
parameters.url += (parameters.url.includes('?') ? '&' : '?') + `site=${siteId}`;
|
||||
parameters.cancelToken = new CancelToken(executor => {
|
||||
cancelHandler.push({
|
||||
executor,
|
||||
url: parameters.url,
|
||||
});
|
||||
});
|
||||
|
||||
axios(parameters)
|
||||
.then(res => resolve(res.data))
|
||||
.catch(error => {
|
||||
if (!axios.isCancel(error)) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
cancelHandler = cancelHandler.filter(req => req.url !== parameters.url);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export default fetcher;
|
||||
@@ -0,0 +1,96 @@
|
||||
.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: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&__score-sign {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import Promise from 'promise-polyfill';
|
||||
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
Promise._unhandledRejectionFn = () => {};
|
||||
/* eslint-enable no-underscore-dangle */
|
||||
|
||||
/* eslint-disable no-extend-native */
|
||||
Promise.prototype.finally = function finallyFn(callback) {
|
||||
const constructor = this.constructor;
|
||||
|
||||
return this.then(
|
||||
(value) => constructor.resolve(callback()).then(() => value),
|
||||
(reason) => constructor.resolve(callback()).then(() => reason)
|
||||
);
|
||||
};
|
||||
/* eslint-enable no-extend-native */
|
||||
|
||||
window.Promise = Promise;
|
||||
@@ -0,0 +1,96 @@
|
||||
import tmpl from 'blueimp-tmpl';
|
||||
|
||||
import { nodeId } from './settings';
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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) : '' %}">
|
||||
<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.scoreSign %}</span>{%= o.score %}
|
||||
<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,
|
||||
scoreSign: comment.score > 0 ? '+' : (comment.score < 0 ? '−' : ''),
|
||||
score: Math.abs(comment.score),
|
||||
time: timeStr,
|
||||
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;
|
||||
};
|
||||
|
||||
if (node) {
|
||||
render();
|
||||
} else {
|
||||
afterInit = render;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
const baseUrl = 'https://demo.remark42.com';
|
||||
const apiBase = '/api/v1'
|
||||
const siteId = 'remark';
|
||||
const nodeId = 'remark42';
|
||||
|
||||
export {
|
||||
baseUrl,
|
||||
siteId,
|
||||
apiBase,
|
||||
nodeId,
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>remark42</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 40px;
|
||||
border: 1px solid #f00000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="remark42"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+9728
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "remark-ui",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production webpack --config ./webpack.config.js",
|
||||
"start": "webpack-dev-server --progress --hot --inline --config ./webpack.config.js",
|
||||
"reinstall": "rm -rf ./node_modules/ && npm install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.1.1",
|
||||
"babel-core": "^6.24.1",
|
||||
"babel-loader": "^7.0.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-preset-env": "^1.5.1",
|
||||
"clean-webpack-plugin": "^0.1.16",
|
||||
"css-loader": "^0.28.0",
|
||||
"extract-text-webpack-plugin": "^3.0.0",
|
||||
"file-loader": "^0.11.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"mimic": "^2.0.2",
|
||||
"node-sass": "^4.5.2",
|
||||
"postcss-csso": "^2.0.0",
|
||||
"postcss-loader": "^2.0.5",
|
||||
"postcss-url": "^6.1.0",
|
||||
"sass-loader": "^6.0.4",
|
||||
"style-loader": "^0.19.1",
|
||||
"webpack": "^3.3.0",
|
||||
"webpack-dev-server": "^2.7.1",
|
||||
"webpack-manifest-plugin": "^1.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.17.1",
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"blueimp-tmpl": "^3.11.0",
|
||||
"promise-polyfill": "^7.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<html>
|
||||
<title>remark</title>
|
||||
|
||||
<head>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/web/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/web/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/web/favicon-16x16.png">
|
||||
<link rel="manifest" href="/web/manifest.json">
|
||||
<link rel="mask-icon" href="/web/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
blah blah blah
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,112 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const CleanPlugin = require('clean-webpack-plugin');
|
||||
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||
const HtmlPlugin = require('html-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const publicFolder = path.resolve(__dirname, 'public');
|
||||
const env = process.env.NODE_ENV || 'dev';
|
||||
const hash = env === 'production' ? '.[chunkhash]' : '.[hash]';
|
||||
|
||||
const extractCSS = new ExtractTextPlugin({
|
||||
filename: `app${hash}.css`,
|
||||
allChunks: true
|
||||
});
|
||||
const cleanPublic = new CleanPlugin(publicFolder);
|
||||
const uglifyJS = new webpack.optimize.UglifyJsPlugin();
|
||||
const ModuleConcatenation = new webpack.optimize.ModuleConcatenationPlugin();
|
||||
const Manifest = new ManifestPlugin();
|
||||
const Html = new HtmlPlugin({ template: path.resolve(__dirname, 'index.ejs') })
|
||||
|
||||
const postcssLoader = {
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
plugins: [
|
||||
require('autoprefixer')({ browsers: ['> 1%'] }),
|
||||
require('postcss-url')({ url: 'inline', maxSize: 5 }),
|
||||
require('postcss-csso')
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
context: __dirname,
|
||||
entry: {
|
||||
app: './app/app',
|
||||
},
|
||||
output: {
|
||||
path: publicFolder,
|
||||
// publicPath: '/path/to/public',
|
||||
filename: `app${hash}.js`
|
||||
},
|
||||
resolve: {
|
||||
modules: [ path.resolve(__dirname), 'node_modules' ]
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /(node_modules|\.vendor\.js$)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['env', {
|
||||
targets: ['> 1%', 'android >= 4.4.4', 'ios >= 9'],
|
||||
useBuiltIns: true,
|
||||
}],
|
||||
],
|
||||
plugins: ['transform-object-rest-spread'],
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [ 'style-loader', 'css-loader', postcssLoader ]
|
||||
// TODO: style-loader must be turned on only for dev; for build we need extractCSS
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
postcssLoader,
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
// data: fs.readFileSync(path.resolve(__dirname, 'common/vars/vars.scss'), 'utf-8')
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|jpeg|gif)$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: `files/[name].[hash].[ext]`
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
cleanPublic,
|
||||
Html, // we should add it only in dev
|
||||
extractCSS,
|
||||
// uglifyJS,
|
||||
ModuleConcatenation,
|
||||
Manifest,
|
||||
],
|
||||
watch: env === 'dev',
|
||||
watchOptions: {
|
||||
ignored: /(node_modules|\.vendor\.js$)/
|
||||
},
|
||||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
contentBase: publicFolder,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user