add draft of web app

This commit is contained in:
igoradamenko
2018-01-06 11:58:24 +02:00
parent 230e06148e
commit 45df3c8f3e
12 changed files with 10199 additions and 17 deletions
+3
View File
@@ -7,3 +7,6 @@ target
debug
debug.test
.vscode
.idea/
/web/node_modules/
/web/public/
+11
View File
@@ -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);
+62
View File
@@ -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;
+96
View File
@@ -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;
}
}
+18
View File
@@ -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;
+96
View File
@@ -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;
}
}
+11
View File
@@ -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,
};
+25
View File
@@ -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>
+9728
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -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"
}
}
-17
View File
@@ -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>
+112
View File
@@ -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,
},
};