Article server based

Required Sync changes!
This commit is contained in:
tmshv committed 2017-05-22 04:50:34 +03:00
1 parent 6acb7ba64d
commit be84058aba
18 files changed
+475 -114

No files matched your search

+2 -4
View File
@@ -13,10 +13,8 @@ const App = ({children, menu}) => (
</header>
<main className="body-wrapper__content">
<section className="content content_thin content--padding-top--menu">
<div className="cloud-page__body">
{children}
</div>
<section className="content content_full content--padding-top--menu">
{children}
</section>
</main>
+24
View File
@@ -0,0 +1,24 @@
const React = require('react')
const getHtml = require('../lib/component').getHtml
const dateFormat = require('../lib/date').dateFormat
const Head = ({url, children}) => (
!url
? <h1>{children}</h1>
: <a href={url}><h2>{children}</h2></a>
)
const Article = ({data, url, title, date}) => (
<article className="article">
<header className="article__head">
<Head url={url}>{title}</Head>
<p className="date">{dateFormat(date)}</p>
</header>
<div className="article__body">
{getHtml(data)}
</div>
</article>
)
module.exports = Article
+27
View File
@@ -0,0 +1,27 @@
const React = require('react')
const Paginator = require('./Paginator')
const Article = require('./Article')
const ArticleList = ({articles, nextPage, prevPage}) => (
<div className="content content_thin">
{!prevPage ? null : (
<Paginator url={`/articles/${prevPage}`} type="top"/>
)}
{articles.map(article => (
<Article
key={article.url}
url={article.url}
title={article.title}
date={article.date}
data={article.post}
/>
))}
{!nextPage ? null : (
<Paginator url={`/articles/${nextPage}`} type="bottom"/>
)}
</div>
)
module.exports = ArticleList
+7 -3
View File
@@ -1,20 +1,24 @@
const React = require('react')
const getPathWithNoTrailingSlas = require('../lib/url').getPathWithNoTrailingSlash
const hasSubmenu = item => !!item.items
const doDrawSubmenu = item => false //item.active && hasSubmenu(item)
const itemUrl = url => getPathWithNoTrailingSlas(url)
const selectedClassName = active => active
const selectedClassName = flag => flag
? 'selected'
: ''
const MenuItem = ({active, text, url}) => (active
? <span>{text}</span>
: <a href={url}>{text}</a>
: <a href={itemUrl(url)}>{text}</a>
)
const Menu = ({items}) => {
const content = items.map((item, index) => (
<li key={index} className={`${item.color} ${selectedClassName(item.active)}`}>
<li key={index}
className={`${item.color} ${selectedClassName(item.highlighted)}`}
>
<MenuItem {...item}/>
{!doDrawSubmenu(item) ? null :
<Submenu key={index} items={item.items}/>
+12
View File
@@ -0,0 +1,12 @@
const React = require('react')
const getHtml = require('../lib/component').getHtml
const Page = ({children}) => (
<div className="content content_thin">
<div className="cloud-page__body">
{getHtml(children)}
</div>
</div>
)
module.exports = Page
+16
View File
@@ -0,0 +1,16 @@
const React = require('react')
const text = ['↑ Предыдущая страница', '↓ Следующая страница']
const style = ['article-page-control__top', 'article-page-control__bottom']
const i = type => type === 'top'
? 0
: 1
const Paginator = ({url, type}) => (
<div className={`article-page-control ${style[i(type)]}`}>
<a href={url}>{text[i(type)]}</a>
</div>
)
module.exports = Paginator
+9 -2
View File
@@ -3,13 +3,20 @@ import {renderToStaticMarkup} from 'react-dom/server'
const App = require('../components/App')
function renderApp({menu, page}) {
function renderApp({menu, component}) {
const app = (
<App menu={menu}>
<div dangerouslySetInnerHTML={{__html: page.data}}/>
{component}
</App>
)
return renderToStaticMarkup(app)
}
function getHtml(html) {
return (
<div dangerouslySetInnerHTML={{__html: html}}/>
)
}
exports.renderApp = renderApp
exports.getHtml = getHtml
+91
View File
@@ -0,0 +1,91 @@
const {readFile} = require('async-file')
const handlebars = require('handlebars')
const url = require('url')
const renderApp = require('../lib/component').renderApp
const getHtml = require('../lib/component').getHtml
const getPathWithNoTrailingSlash = require('../lib/url').getPathWithNoTrailingSlash
const urlToPattern = require('../lib/url').urlToPattern
const isMatchPathPattern = require('../lib/url').isMatchPathPattern
const isEqualPate = require('../lib/url').isEqualPath
const {compose, any} = require('../lib/common')
const menuModel = require('../models/menu').default
const config = require('../config')
function isActive(path, menuItem) {
return isEqualPate(path, menuItem.url, true)
}
function isHighlighted(path, menuItem) {
return any(flatMenuItem(menuItem)
.map(i => isMatchPathPattern(
compose(urlToPattern, getPathWithNoTrailingSlash)(i.url),
path,
true
))
)
}
function flatMenuItem(item) {
const rootUrl = item.url
const flatUrls = items => {
return items.map(item => ({
...item,
url: url.resolve(rootUrl, item.url)
}))
}
return !item.items
? [item]
: item.items.reduce((acc, i) => [...acc, ...compose(flatUrls, flatMenuItem)(i)], [item])
}
/**
* Takes plain menu model and sample path and defines is an item of menu should be active
*
* @param path
* @param menu
* @return {{items}}
*/
function buildMenu(path, menu) {
const items = menu.map(item => ({
...item,
active: isActive(path, item),
highlighted: isHighlighted(path, item),
}))
return {items}
}
const getTitle = meta => 'title' in meta
? meta.title
: 'Шлиссельбургская художественная школа'
const isHtml = data => typeof data === 'string'
async function render(path, data, meta) {
const component = isHtml(data)
? getHtml(data)
: data
const menu = buildMenu(path, menuModel)
const content = renderApp({menu, component})
const source = await readFile(config.viewMain, 'utf-8')
const template = handlebars.compile(source)
const metaData = {
...meta,
description: 'Сайт Шлиссельбургской художественной школы',
image: 'https://art.shlisselburg.org/entrance.jpg',
url: `https://art.shlisselburg.org${path}`,
}
return template({
content,
title: getTitle(meta),
meta: metaData,
})
}
exports.render = render
+27 -1
View File
@@ -1,7 +1,10 @@
function getPathWithNoTrailingSlash(path) {
return path.charAt(path.length - 1) === '/'
path = path.charAt(path.length - 1) === '/'
? path.substr(0, path.length - 1)
: path
return path.length === 0
? '/'
: path
}
function isEqualPath(a, b, ignoreTrailingSlash = false) {
@@ -10,5 +13,28 @@ function isEqualPath(a, b, ignoreTrailingSlash = false) {
: a === b
}
function isMatchPathPattern(pattern, path, ignoreTrailingSlash = false) {
const test = ignoreTrailingSlash
? getPathWithNoTrailingSlash(path)
: path
return pattern.test(test)
}
/**
*
* /article/* -> ^\/article\/[/\w\d-_]+\/?$
*
* @param url
* @return {RegExp}
*/
function urlToPattern(url) {
const u = url.replace('/*', '[/\\w\\d-_]+')
const pattern = `^${u}/?$`
return new RegExp(pattern)
}
exports.getPathWithNoTrailingSlash = getPathWithNoTrailingSlash
exports.isMatchPathPattern = isMatchPathPattern
exports.urlToPattern = urlToPattern
exports.isEqualPath = isEqualPath
+6 -7
View File
@@ -5,13 +5,10 @@ export default [
color: 'blue',
items: [
{url: '/summer-school/2017', text: 'Летние каникулы 2017'},
{url: '/articles/*', text: 'Статьи'},
{url: '/article/*', text: ''},
],
},
{
'text': 'События',
'url': '/events',
'color': 'orange'
},
}, ,
{
'text': 'Расписание',
'url': '/schedule',
@@ -60,7 +57,7 @@ export default [
{
url: '/info/',
text: 'Сведения об образовательной организации',
color: 'gray',
color: 'orange',
items: [
{url: 'main', text: 'Основные сведения'},
{url: 'structure', text: 'Структура и органы управления образовательной организацией'},
@@ -75,6 +72,8 @@ export default [
{url: 'tools', text: 'Список рекомендованных материалов'},
{url: 'history', text: 'История школы'},
{url: '*', text: ''},
]
},
{
+131 -23
View File
@@ -1,28 +1,136 @@
import route from 'koa-route';
import {c} from '../core/db';
import {accepts, index} from './';
const React = require('react')
import {get} from 'koa-route'
import {c} from '../core/db'
const Article = require('../components/Article')
const ArticleList = require('../components/ArticleList')
const Paginator = require('../components/Paginator')
const {render} = require('../lib/render')
const getPathWithNoTrailingSlash = require('../lib/url').getPathWithNoTrailingSlash
const timestamp = require('../lib/date').timestamp
const {sortBy} = require('../utils/sort')
const accepts = require('./index').accepts
async function exists(ctx, id) {
let i = await article(id);
return i ? true : false;
const sortArticleByDate = sortBy(
i => timestamp(new Date(i.date))
)
const articleUrl = article => `/article/${article.id}`
async function findArticle(id) {
return c('articles').findOne({id: id})
}
async function article(id){
let i = await c('events').findOne({id: id});
if (!i) {
i = await c('timeline').findOne({id: id});
}
return i;
async function findArticlesNin(nin, skip, limit, sort) {
return c('articles')
.find({
_id: {$nin: nin}
})
.sort(sort)
.skip(skip)
.limit(limit)
.toArray()
}
export default function () {
return route.get('/article/:id', accepts({
'text/html': index(exists),
'text/plain': index(exists),
'application/json': async (ctx, id) => {
let i = await article(id);
if (!i) ctx.status = 404;
else ctx.body = i;
}
}))
};
async function totalArticles() {
return c('articles')
.find({})
.count()
}
async function findPinned(page) {
const now = new Date()
return page !== 1
? []
: await c('articles')
.find({until: {$gte: now}})
.sort({date: -1})
.toArray()
}
function getMeta(article) {
return {
title: article.title,
}
}
function getArticles(pageSize) {
return get('/articles/:page?', async (ctx, page) => {
const path = getPathWithNoTrailingSlash(ctx.path)
page = parseInt(page) || 1
const limit = pageSize
const skip = (page - 1) * pageSize
const pinnedArticles = await findPinned(page)
const total = await totalArticles()
const totalPages = total / pageSize
const id = i => i._id
const pinnedIds = pinnedArticles.map(id)
const articles = await findArticlesNin(pinnedIds, skip, limit, {date: -1})
const prevPage = page > 1
? page - 1
: null
const nextPage = page < totalPages
? page + 1
: null
const content = [
...pinnedArticles.sort(sortArticleByDate),
...articles
]
.map(article => ({
...article,
url: articleUrl(article),
}))
const Component = (
<ArticleList
articles={content}
prevPage={prevPage}
nextPage={nextPage}
/>
)
ctx.body = await render(path, Component, getMeta({title: `Статьи`}))
})
}
function getArticle() {
return get('/article/:id', accepts({
'text/html': async (ctx, id) => {
const path = getPathWithNoTrailingSlash(ctx.path)
const article = await findArticle(id)
if (article) {
const Component = (
<div className="content content_thin">
<Article
title={article.title}
date={article.date}
data={article.post}
/>
</div>
)
ctx.type = 'text/html'
ctx.body = await render(path, Component, getMeta(article))
} else {
ctx.status = 404
}
},
'application/json': async (ctx, id) => {
const article = await findArticle(id)
if (article) {
ctx.body = article
} else {
ctx.status = 404
}
}
}))
}
exports.getArticles = getArticles
exports.getArticle = getArticle
+88
View File
@@ -0,0 +1,88 @@
const React = require('react')
const Article = require('../components/Article')
const {get} = require('koa-route')
const {c} = require('../core/db')
const {render} = require('../lib/render')
const getPathWithNoTrailingSlash = require('../lib/url').getPathWithNoTrailingSlash
const timestamp = require('../lib/date').timestamp
const {sortBy} = require('../utils/sort')
const sortArticleByDate = sortBy(
i => timestamp(new Date(i.date))
)
const articleUrl = article => `/article/${article.id}`
async function findArticlesNin(nin, skip, limit, sort) {
return c('articles')
.find({
_id: {$nin: nin}
})
.sort(sort)
.skip(skip)
.limit(limit)
.toArray()
}
async function findPinned(page) {
const now = new Date()
return page !== 1
? []
: await c('articles')
.find({until: {$gte: now}})
.sort({date: -1})
.toArray()
}
function getComponent(path, article) {
return (
<Article
url={path}
title={article.title}
date={article.date}
data={article.post}
/>
)
}
function getMeta(article) {
return {
title: article.title,
}
}
function getHome(pageSize) {
return get('/', async (ctx, page) => {
const path = getPathWithNoTrailingSlash(ctx.path) || '/'
page = parseInt(page) || 1
const limit = pageSize
const skip = (page - 1) * pageSize
const pinnedArticles = await findPinned(page)
const id = i => i._id
const pinnedIds = pinnedArticles.map(id)
const articles = await findArticlesNin(pinnedIds, skip, limit, {date: -1})
const content = [
...pinnedArticles.sort(sortArticleByDate),
...articles
]
.map(article => getComponent(articleUrl(article), article))
const Component = (
<div className="content content_thin">
<div className="hudozka-title">
<p>МБУДО</p>
<h1>Шлиссельбургская детская художественная&nbsp;школа</h1>
</div>
{content}
</div>
)
ctx.body = await render(path, Component, getMeta({title: 'Шлиссельбургская ДХШ'}))
})
}
exports.getHome = getHome
-7
View File
@@ -1,6 +1,5 @@
import {readFile} from 'async-file'
import handlebars from 'handlebars'
import route from 'koa-route'
import compose from 'koa-compose'
import {map, assign} from '../utils/common'
@@ -8,20 +7,15 @@ import {index as indexFile} from '../config'
import sitemap from './sitemap'
import schedule from './schedule'
import {getPageUrls} from '../core/pages'
import news from './news'
import gallery from './gallery'
import documents from './documents'
import teachers from './teachers'
import events from './events'
import articles from './articles'
import pages from './pages'
let main = () => route.get('/', index())
export function routes(store) {
return compose([
main(),
sitemap(),
schedule(),
news(),
@@ -29,7 +23,6 @@ export function routes(store) {
documents(store),
teachers(),
events(),
articles(),
pages(),
])
}
+13 -53
View File
@@ -1,55 +1,25 @@
const {readFile} = require('async-file')
const handlebars = require('handlebars')
const React = require('react')
const Page = require('../components/Page')
const {get} = require('koa-route')
const {c} = require('../core/db')
const url = require('url')
const renderApp = require('../lib/component').renderApp
const isEqualPath = require('../lib/url').isEqualPath
const getPathWithNoTrailingSlash = require('../lib/url').getPathWithNoTrailingSlash
const {compose, any} = require('../lib/common')
const {render} = require('../lib/render')
const menuModel = require('../models/menu').default
const config = require('../config')
async function getPage(path) {
return await c('pages').findOne({url: path})
}
function isActive(path, menuItem) {
return any(flatMenuItem(menuItem)
.map(i => isEqualPath(path, i.url, true))
)
}
function flatMenuItem(item) {
const rootUrl = item.url
const flatUrls = items => {
return items.map(item => ({
...item,
url: url.resolve(rootUrl, item.url)
}))
function getPageMeta(page) {
return {
title: page.title,
}
return !item.items
? [item]
: item.items.reduce((acc, i) => [...acc, ...compose(flatUrls, flatMenuItem)(i)], [item])
}
/**
* Takes plain menu model and sample path and defines is an item of menu should be active
*
* @param path
* @param menu
* @return {{items}}
*/
function buildMenu(path, menu) {
const items = menu.map(item => ({
...item,
active: isActive(path, item)
}))
return {items}
function getComponent(page) {
return <Page>{page.data}</Page>
}
module.exports = function () {
@@ -57,22 +27,12 @@ module.exports = function () {
const path = getPathWithNoTrailingSlash(ctx.path)
const page = await getPage(path)
if (page) {
const menu = buildMenu(path, menuModel)
const content = renderApp({menu, page})
const source = await readFile(config.viewMain, 'utf-8')
const template = handlebars.compile(source)
const title = page.title
const meta = {
title,
description: title,
image: 'https://art.shlisselburg.org/entrance.jpg',
url: `https://art.shlisselburg.org${path}`,
}
ctx.type = 'text/html'
ctx.body = template({content, title, meta})
ctx.body = await render(
path,
getComponent(page),
getPageMeta(page),
)
} else {
ctx.status = 404
}
+5
View File
@@ -21,6 +21,8 @@ import handlebars from 'handlebars'
const config = require('./config')
const error = require('./middlewares/error')
const home = require('./routes/home')
const article = require('./routes/articles')
const dirPublic = path.join(__dirname, '../public')
const dirTemplates = path.join(__dirname, 'templates')
@@ -52,6 +54,9 @@ export default function (store) {
app.use(queryObject())
app.use(error.notFound(config.view404))
app.use(home.getHome(5))
app.use(article.getArticles(5))
app.use(article.getArticle())
app.use(routes(store))
return Server(app.callback())
+7 -1
View File
@@ -6,9 +6,15 @@
&__head {
margin-bottom: 1.25em;
h3 {
h1 {
font-size: $biggest-font-size;
text-align: center;
margin-bottom: $half-margin;
}
h2 {
font-size: $big-font-size;
text-align: center;
}
.date {
-6
View File
@@ -49,12 +49,6 @@
font-weight: bold;
border-top: $menu-colored-mark-thickness solid $light-color;
a:link, a:visited {
color: $dark-color;
text-decoration: none;
border-bottom: none;
}
&.blue {
border-top: $menu-colored-mark-thickness solid $blue-color;
}
+10 -7
View File
@@ -1,20 +1,23 @@
@import "variables";
.main-image{
display: none;
width: 100%;
display: flex;
justify-content: center;
overflow: hidden;
//display: flex;
//justify-content: center;
//overflow: hidden;
img {
width: 100%;
max-width: 1280px;
}
&__content {
min-width: 1280px;
width: 1280px;
}
//&__content {
// min-width: 1280px;
// width: 1280px;
//}
}
@media (max-width: $mobile-width) {