diff --git a/src/client/app.js b/src/client/app.js index 17587dcd..a3017cc8 100644 --- a/src/client/app.js +++ b/src/client/app.js @@ -233,16 +233,25 @@ app.config(($locationProvider, $routeProvider) => { }) $routeProvider.otherwise({ - templateUrl: '/404.html' + template: '', + controller: function($window) { + console.log('otherwise') + $window.location.reload() + } }) }) -app.run(($location, $rootScope, $http) => { +app.run(($window, $location, $rootScope, $http) => { $rootScope.$on('$routeChangeSuccess', (event, current) => { const url = location.href - const title = current['$$route'].title - if (title) $rootScope.title = title + try { + const title = current['$$route'].title + + if (title) $rootScope.title = title + } catch (e) { + + } setTimeout(() => { try { diff --git a/src/components/App.js b/src/components/App.js new file mode 100644 index 00000000..51b72d21 --- /dev/null +++ b/src/components/App.js @@ -0,0 +1,31 @@ +const React = require('react') +const Menu = require('./Menu') +const Footer = require('./Footer') + +const App = ({children, menu}) => ( +
+
+
+
+ +
+
+
+ +
+
+
+ {children} +
+
+
+ +
+) + +module.exports = App diff --git a/src/components/Footer.js b/src/components/Footer.js new file mode 100644 index 00000000..68856c99 --- /dev/null +++ b/src/components/Footer.js @@ -0,0 +1,31 @@ +const React = require('react') + +const mailto = email => `mailto:${email}` + +const Footer = ({children, address, telephone, email, showAuthor}) => ( + +) + +module.exports = Footer diff --git a/src/components/Menu.js b/src/components/Menu.js new file mode 100644 index 00000000..5438513f --- /dev/null +++ b/src/components/Menu.js @@ -0,0 +1,39 @@ +const React = require('react') + +const hasSubmenu = item => !!item.items +const doDrawSubmenu = item => false //item.active && hasSubmenu(item) + +const selectedClassName = active => active + ? 'selected' + : '' + +const MenuItem = ({active, text, url}) => (active + ? {text} + : {text} +) + +const Menu = ({items}) => { + const content = items.map((item, index) => ( +
  • + + {!doDrawSubmenu(item) ? null : + + } +
  • + )) + return ( + {content} + ) +} + +const Submenu = ({items}) => ( + + {items.map((item, index) => ( +
  • + +
  • + ))} +
    +) + +module.exports = Menu diff --git a/src/lib/common.js b/src/lib/common.js new file mode 100644 index 00000000..0fd2b0db --- /dev/null +++ b/src/lib/common.js @@ -0,0 +1,36 @@ +function compose(...fns) { + return value => [...fns] + .reverse() + .reduce((acc, fn) => fn(acc), value) +} + +/** + * AND operator for list of values + * + * [true, true, true, ...] -> true + * [false, true, true, ...] -> false + * + * @param list + * @return {boolean} + */ +function all(list) { + return list.reduce((t, i) => t && i, true) +} + +/** + * OR operator for list of values + * + * [true, true, true, ...] -> true + * [false, true, true, ...] -> true + * [false, false, false, ...] -> false + * + * @param list + * @return {boolean} + */ +function any(list) { + return list.reduce((t, i) => t || i, false) +} + +exports.compose = compose +exports.all = all +exports.any = any diff --git a/src/lib/component.js b/src/lib/component.js new file mode 100644 index 00000000..8a2dcd47 --- /dev/null +++ b/src/lib/component.js @@ -0,0 +1,15 @@ +import React from 'react' +import {renderToStaticMarkup} from 'react-dom/server' + +const App = require('../components/App') + +function renderApp({menu, page}) { + const app = ( + +
    + + ) + return renderToStaticMarkup(app) +} + +exports.renderApp = renderApp diff --git a/src/lib/url.js b/src/lib/url.js new file mode 100644 index 00000000..1b2c3d4a --- /dev/null +++ b/src/lib/url.js @@ -0,0 +1,14 @@ +function getPathWithNoTrailingSlash(path) { + return path.charAt(path.length - 1) === '/' + ? path.substr(0, path.length - 1) + : path +} + +function isEqualPath(a, b, ignoreTrailingSlash = false) { + return ignoreTrailingSlash + ? getPathWithNoTrailingSlash(a) === getPathWithNoTrailingSlash(b) + : a === b +} + +exports.getPathWithNoTrailingSlash = getPathWithNoTrailingSlash +exports.isEqualPath = isEqualPath diff --git a/src/middlewares/error.js b/src/middlewares/error.js new file mode 100644 index 00000000..9e36622a --- /dev/null +++ b/src/middlewares/error.js @@ -0,0 +1,18 @@ +const {readFile} = require('async-file') +const handlebars = require('handlebars') + +function notFound(templateFile) { + return async (ctx, next) => { + await next() + + if (ctx.status === 404) { + const source = await readFile(templateFile, 'utf-8') + const template = handlebars.compile(source) + + ctx.type = 'text/html' + ctx.body = template() + } + } +} + +exports.notFound = notFound diff --git a/src/models/menu.js b/src/models/menu.js index 1c723afa..ed542083 100644 --- a/src/models/menu.js +++ b/src/models/menu.js @@ -1,8 +1,11 @@ export default [ { - 'text': 'Школа', - 'url': '/', - 'color': 'blue' + url: '/', + text: 'Школа', + color: 'blue', + items: [ + {url: '/summer-school/2017', text: 'Летние каникулы 2017'}, + ], }, { 'text': 'События', @@ -20,9 +23,24 @@ export default [ 'color': 'yellow' }, { - 'text': 'Предметы', - 'url': '/courses', - 'color': 'magenta' + url: '/courses/', + text: 'Предметы', + color: 'magenta', + items: [ + {url: 'ceramics', text: 'Керамика'}, + {url: 'batik', text: 'Живопись'}, + {url: 'easel-composition', text: 'Живопись'}, + {url: 'cg', text: 'Живопись'}, + {url: 'scale-modeling', text: 'Живопись'}, + {url: 'drawing', text: 'Живопись'}, + {url: 'costume-composition', text: 'Живопись'}, + {url: 'felting', text: 'Живопись'}, + {url: 'art-history', text: 'Живопись'}, + + {url: 'perishki', text: 'Живопись'}, + {url: 'matreshki', text: 'Живопись'}, + {url: 'listochki', text: 'Живопись'}, + ] }, { 'text': 'Коллектив', @@ -40,9 +58,24 @@ export default [ 'color': 'yellow' }, { - 'text': 'Сведения об образовательной организации', - 'url': '/info', - 'color': 'gray' + url: '/info/', + text: 'Сведения об образовательной организации', + color: 'gray', + items: [ + {url: 'main', text: 'Основные сведения'}, + {url: 'structure', text: 'Структура и органы управления образовательной организацией'}, + {url: 'education', text: 'Образование'}, + {url: 'standards', text: 'Образовательные стандарты'}, + {url: '/collective', text: 'Руководство. Педагогический (научно-педагогический) состав'}, + {url: 'support', text: 'Материально-техническое обеспечение образовательного процесса'}, + {url: 'grants', text: 'Стипендии и иные виды материальной поддержки'}, + {url: 'paid-services', text: 'Платные образовательные услуги'}, + {url: 'finances', text: 'Финансово-хозяйственная деятельность'}, + {url: 'vacancy', text: 'Вакантные места для приема'}, + + {url: 'tools', text: 'Список рекомендованных материалов'}, + {url: 'history', text: 'История школы'}, + ] }, { 'text': 'Текущий контроль', diff --git a/src/routes/404.js b/src/routes/404.js deleted file mode 100644 index bddf1507..00000000 --- a/src/routes/404.js +++ /dev/null @@ -1,26 +0,0 @@ -import {index, accepts} from './'; - -export default function () { - return async(ctx) => { - if (ctx.status != 404) return; - - ctx.status = 404; - ctx.body = 'lol found'; - - await accepts({ - 'text/html': index(), - 'text/plain': async ctx => { - ctx.type = 'text/plain'; - ctx.body = 'Page not found'; - }, - - 'application/json': async ctx => { - ctx.type = 'application/json'; - ctx.body = { - error: 404, - message: 'Not Found' - }; - } - })(ctx); - }; -}; \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js index 4ddaea8e..0e061dd5 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -11,7 +11,6 @@ import schedule from './schedule' import {getPageUrls} from '../core/pages' import news from './news' import gallery from './gallery' -import error404 from './404' import documents from './documents' import teachers from './teachers' import events from './events' @@ -32,7 +31,6 @@ export function routes(store) { events(), articles(), pages(), - error404() ]) } @@ -96,6 +94,5 @@ export function json(fn) { } async function getInitialData() { - const pages = await getPageUrls() - return {pages: pages} + return {pages: []} } diff --git a/src/routes/pages.js b/src/routes/pages.js index eb2f671c..253adbf7 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -1,24 +1,71 @@ -import route from 'koa-route' -import {c} from '../core/db' -import {accepts, index} from './' +const {readFile} = require('async-file') +const handlebars = require('handlebars') -async function exists(ctx) { - const page = await getPage(ctx.path) - return Boolean(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 menuModel = require('../models/menu').default async function getPage(path) { return await c('pages').findOne({url: path}) } -export default function () { - return route.get('*', accepts({ - 'text/html': index(exists), - 'text/plain': index(exists), - 'application/json': async(ctx) => { - const page = await getPage(ctx.path) - if (!page) ctx.status = 404 - else ctx.body = page - } +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) + })) + } + + 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} +} + +module.exports = function () { + return get('*', async ctx => { + 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('src/views/main.hbs', 'utf-8') + const template = handlebars.compile(source) + + ctx.type = 'text/html' + ctx.body = template({content}) + } else { + ctx.status = 404 + } + }) +} diff --git a/src/server.js b/src/server.js index 6b2b6ba6..0e3f34f6 100644 --- a/src/server.js +++ b/src/server.js @@ -19,6 +19,8 @@ import {redirect} from './routes/redirect' import {services as serviceKeys, authChecker} from './core/service' import handlebars from 'handlebars' +const error = require('./middlewares/error') + const dirPublic = path.join(__dirname, '../public') const dirTemplates = path.join(__dirname, 'templates') @@ -47,6 +49,8 @@ export default function (store) { app.use($(redirect(redirectionTable))) app.use($(helmet())) app.use(queryObject()) + + app.use(error.notFound('src/views/404.hbs')) app.use(routes(store)) return Server(app.callback()) diff --git a/src/style/components/main-menu.scss b/src/style/components/main-menu.scss index e050f28b..fff6906d 100644 --- a/src/style/components/main-menu.scss +++ b/src/style/components/main-menu.scss @@ -7,11 +7,30 @@ list-style: none; - li { + .main-menu__submenu { + background-color: rgba(255, 255, 255, 0.95); + + display: block; + position: absolute; + top: 100%; + left: -1em; + + padding: 0 0.5em 0.5em; + + & > li { + display: block; + padding: 0.3em 0.2em 0.5em; + margin: 0 0.5em; + } + } + + & > li { display: inline-block; padding: 0.3em 0.2em 0.5em; margin: 0 0.5em; + position: relative; + &:first-child { margin-left: 1em; } diff --git a/src/style/footer.scss b/src/style/footer.scss index 9159a00c..31e964fa 100644 --- a/src/style/footer.scss +++ b/src/style/footer.scss @@ -36,4 +36,13 @@ footer { box-sizing: padding-box; } } + + &.footer--centered { + .copyright { + display: flex; + flex-wrap: wrap; + align-items: baseline; + justify-content: center; + } + } } \ No newline at end of file diff --git a/src/views/404.hbs b/src/views/404.hbs new file mode 100644 index 00000000..b19b31af --- /dev/null +++ b/src/views/404.hbs @@ -0,0 +1,58 @@ + + + + + + + + + + + + + По вашему запросу ничего не найдено — Шлиссельбургская ДХШ + + + + + + + + + + + + + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +

    Страница не найдена.

    + +
    + + +
    + + +
    +
    +
    +
    + +
    + +
    +
    + + + diff --git a/src/views/main.hbs b/src/views/main.hbs new file mode 100644 index 00000000..6abcb2cf --- /dev/null +++ b/src/views/main.hbs @@ -0,0 +1,61 @@ + + + + + + + + + + + + + Шлиссельбургская Детская Художественная Школа + + + + + + + + + + + + + + + + +{{{content}}} + + + + +