diff --git a/src/components/App.js b/src/components/App.js index 51b72d21..c649f54b 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -13,10 +13,8 @@ const App = ({children, menu}) => (
-
-
- {children} -
+
+ {children}
diff --git a/src/components/Article.js b/src/components/Article.js new file mode 100644 index 00000000..d07550f7 --- /dev/null +++ b/src/components/Article.js @@ -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 + ?

{children}

+ :

{children}

+) + +const Article = ({data, url, title, date}) => ( +
+
+ {title} +

{dateFormat(date)}

+
+ +
+ {getHtml(data)} +
+
+) + +module.exports = Article diff --git a/src/components/ArticleList.js b/src/components/ArticleList.js new file mode 100644 index 00000000..af3ebb56 --- /dev/null +++ b/src/components/ArticleList.js @@ -0,0 +1,27 @@ +const React = require('react') +const Paginator = require('./Paginator') +const Article = require('./Article') + +const ArticleList = ({articles, nextPage, prevPage}) => ( +
+ {!prevPage ? null : ( + + )} + + {articles.map(article => ( +
+ ))} + + {!nextPage ? null : ( + + )} +
+) + +module.exports = ArticleList diff --git a/src/components/Menu.js b/src/components/Menu.js index 5438513f..80510006 100644 --- a/src/components/Menu.js +++ b/src/components/Menu.js @@ -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 ? {text} - : {text} + : {text} ) const Menu = ({items}) => { const content = items.map((item, index) => ( -
  • +
  • {!doDrawSubmenu(item) ? null : diff --git a/src/components/Page.js b/src/components/Page.js new file mode 100644 index 00000000..1e9170d5 --- /dev/null +++ b/src/components/Page.js @@ -0,0 +1,12 @@ +const React = require('react') +const getHtml = require('../lib/component').getHtml + +const Page = ({children}) => ( +
    +
    + {getHtml(children)} +
    +
    +) + +module.exports = Page diff --git a/src/components/Paginator.js b/src/components/Paginator.js new file mode 100644 index 00000000..be6d438f --- /dev/null +++ b/src/components/Paginator.js @@ -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}) => ( +
    + {text[i(type)]} +
    +) + +module.exports = Paginator diff --git a/src/lib/component.js b/src/lib/component.js index 8a2dcd47..54ba3de1 100644 --- a/src/lib/component.js +++ b/src/lib/component.js @@ -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 = ( -
    + {component} ) return renderToStaticMarkup(app) } +function getHtml(html) { + return ( +
    + ) +} + exports.renderApp = renderApp +exports.getHtml = getHtml diff --git a/src/lib/render.js b/src/lib/render.js new file mode 100644 index 00000000..95d42f97 --- /dev/null +++ b/src/lib/render.js @@ -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 diff --git a/src/lib/url.js b/src/lib/url.js index 1b2c3d4a..0452ab69 100644 --- a/src/lib/url.js +++ b/src/lib/url.js @@ -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 diff --git a/src/models/menu.js b/src/models/menu.js index ed542083..ebc9e1c7 100644 --- a/src/models/menu.js +++ b/src/models/menu.js @@ -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: ''}, ] }, { diff --git a/src/routes/articles.js b/src/routes/articles.js index 0af181b1..d09f5e41 100644 --- a/src/routes/articles.js +++ b/src/routes/articles.js @@ -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 = ( + + ) + + 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 = ( +
    +
    +
    + ) + + 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 diff --git a/src/routes/home.js b/src/routes/home.js new file mode 100644 index 00000000..b907080d --- /dev/null +++ b/src/routes/home.js @@ -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 ( +
    + ) +} + +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 = ( +
    +
    +

    МБУДО

    +

    Шлиссельбургская детская художественная школа

    +
    + + {content} +
    + ) + + ctx.body = await render(path, Component, getMeta({title: 'Шлиссельбургская ДХШ'})) + }) +} + +exports.getHome = getHome diff --git a/src/routes/index.js b/src/routes/index.js index 0e061dd5..0f256536 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -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(), ]) } diff --git a/src/routes/pages.js b/src/routes/pages.js index b2393d0e..5845dc67 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -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.data} } 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 } diff --git a/src/server.js b/src/server.js index 10082d4f..52d5ded3 100644 --- a/src/server.js +++ b/src/server.js @@ -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()) diff --git a/src/style/components/article.scss b/src/style/components/article.scss index 021b1f3a..93662ede 100644 --- a/src/style/components/article.scss +++ b/src/style/components/article.scss @@ -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 { diff --git a/src/style/components/main-menu.scss b/src/style/components/main-menu.scss index fff6906d..0d06b012 100644 --- a/src/style/components/main-menu.scss +++ b/src/style/components/main-menu.scss @@ -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; } diff --git a/src/style/main-image.scss b/src/style/main-image.scss index eb6af9b4..6b7bf1c7 100644 --- a/src/style/main-image.scss +++ b/src/style/main-image.scss @@ -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) {