diff --git a/src/client/app.js b/src/client/app.js index 907d3bed..f9e24d1c 100644 --- a/src/client/app.js +++ b/src/client/app.js @@ -54,7 +54,7 @@ import API from './services/api' import Theme from './services/theme' import {sortByPattern} from '../utils/sort' -import {documentsSortPattern, collectiveSortPattern} from './config' +import {documentsSortPattern} from './config' let app = angular.module('hudozka', [ 'ngRoute', @@ -92,42 +92,6 @@ app.config(($locationProvider, $routeProvider) => { .list() .then(i => i.data) } - }, - { - name: '/awards', - template: '', - title: 'Награды', - resolve: { - awards: api => api.document - .awards() - .then(i => i.data), - } - }, - { - name: '/documents', - template: '', - title: 'Документы', - resolve: { - documents: api => api.document - .documents() - .then(i => i.data) - .then(i => { - return sortByPattern(i, documentsSortPattern, i => i.category) - }) - } - }, - { - name: '/documents/:id', - template: '', - resolve: { - document: ($route, api) => { - let id = $route.current.params.id - - return api.document - .id(id) - .then(i => i.data) - } - } } ] .concat(pages) diff --git a/src/components/Document.js b/src/components/Document.js new file mode 100644 index 00000000..1b3a473b --- /dev/null +++ b/src/components/Document.js @@ -0,0 +1,23 @@ +const React = require('react') +const {ext, size} = require('../lib/file') +const {titleCase} = require('../lib/string') + +const Document = ({title, url, imageUrl, fileUrl, fileName, fileSize}) => ( +
+
+ + {title}/ + +
+ +
+ {titleCase(title)} +
+ +
+ {ext(fileName)} ({size(fileSize)}) +
+
+) + +module.exports = Document diff --git a/src/components/DocumentList.js b/src/components/DocumentList.js new file mode 100644 index 00000000..2598adc5 --- /dev/null +++ b/src/components/DocumentList.js @@ -0,0 +1,20 @@ +const React = require('react') +const Document = require('./DocumentListItem') + +const DocumentList = ({documents, name}) => ( +
+
+

{name}

+
+ +
+ {documents.map((d, i) => ( +
+ +
+ ))} +
+
+) + +module.exports = DocumentList diff --git a/src/components/DocumentListItem.js b/src/components/DocumentListItem.js new file mode 100644 index 00000000..21ef96d2 --- /dev/null +++ b/src/components/DocumentListItem.js @@ -0,0 +1,23 @@ +const React = require('react') +const {ext, size} = require('../lib/file') +const {titleCase} = require('../lib/string') + +const Document = ({title, url, imageUrl, fileUrl, fileName, fileSize}) => ( +
+ +
+ {title}/ +
+
+ +
+ {titleCase(title)} +
+ +
+ {ext(fileName)} ({size(fileSize)}) +
+
+) + +module.exports = Document diff --git a/src/core/Document.js b/src/core/Document.js new file mode 100644 index 00000000..b62873b4 --- /dev/null +++ b/src/core/Document.js @@ -0,0 +1,54 @@ +const Data = require('./Data') +const Image = require('./Image') +const {find, findOne} = require('../lib/store') + +const store = () => Data.getStore(Document) + +class Document { + static async find(query, options = {}) { + const items = await find(store(), query, options) + + return Promise.all(items.map(processDocument)) + } + + static async findById(id) { + const data = await findOne(store(), {id}) + if (!data) return null + + return processDocument(data) + } + + constructor(data) { + this.id = data.id + this.hash = data.hash + this.file = new DocumentFile(data.file) + this.type = data.type + this.title = data.title + this.category = data.category + this.url = data.url + + this.preview = data.preview + this.viewUrl = `/document/${this.id}` + } +} + +class DocumentFile { + constructor(data) { + this.name = data.name + this.size = data.size + } +} + +const + processDocument = async data => { + const imageId = data.preview + const preview = await Image.findById(imageId) + + return new Document({ + ...data, + preview, + }) + } + +module + .exports = Document diff --git a/src/core/db.js b/src/core/db.js index ae645b73..59ccd500 100644 --- a/src/core/db.js +++ b/src/core/db.js @@ -1,6 +1,7 @@ const Data = require('./Data') const Album = require('./Album') const Teacher = require('./Teacher') +const Document = require('./Document') const Image = require('./Image') const {MongoClient, ObjectId} = require('mongodb') @@ -30,6 +31,7 @@ function init(db) { Data.setStore(Album, db.collection('albums')) Data.setStore(Image, db.collection('images')) Data.setStore(Teacher, db.collection('collective')) + Data.setStore(Document, db.collection('documents')) return db } diff --git a/src/lib/array.js b/src/lib/array.js new file mode 100644 index 00000000..78715bd1 --- /dev/null +++ b/src/lib/array.js @@ -0,0 +1,12 @@ +function splitBy(key) { + return items => { + return items.reduce((acc, item) => { + const name = key(item) + const items = acc.get(name) || [] + + return acc.set(key(item), [...items, item]) + }, new Map()) + } +} + +exports.splitBy = splitBy diff --git a/src/lib/file.js b/src/lib/file.js new file mode 100644 index 00000000..4079f9b8 --- /dev/null +++ b/src/lib/file.js @@ -0,0 +1,27 @@ +function ext(filname) { + let getExt = name => /\.(\w+)$/.exec(name)[1] + let defaultExt = ext => ext.toUpperCase() + + const exts = ['png', 'pdf', 'jpg', 'jpeg', 'gif', 'doc'] + const extd = ['ПНГ', 'ПДФ', 'ЖПГ', 'ЖПГ', 'ГИФ', 'ДОК'] + + let ext = getExt(filname) + const i = exts.indexOf(ext) + + return i > -1 + ? extd[i] + : defaultExt(ext) +} + +function size(bytes, precision = 1) { + if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '' + + const units = ['байт', 'КБ', 'МБ', 'ГБ', 'ТБ', 'ПБ'] + const number = Math.floor(Math.log(bytes) / Math.log(1024)) + + const size = (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + return `${size} ${units[number]}` +} + +exports.ext = ext +exports.size = size diff --git a/src/lib/string.js b/src/lib/string.js new file mode 100644 index 00000000..9a8a5493 --- /dev/null +++ b/src/lib/string.js @@ -0,0 +1,12 @@ +function titleCase(str) { + return str.charAt(0).toUpperCase() + str.slice(1) +} + +function trim(string) { + return string + .replace(/^\s+/, '') + .replace(/\s+$/, '') +} + +exports.titleCase = titleCase +exports.trim = trim diff --git a/src/models/menu.js b/src/models/menu.js index 768a2371..30dda425 100644 --- a/src/models/menu.js +++ b/src/models/menu.js @@ -56,9 +56,12 @@ export default [ ] }, { - 'text': 'Документы', - 'url': '/documents', - 'color': 'yellow' + text: 'Документы', + url: '/documents', + color: 'yellow', + items: [ + {url: '/document/*', text: ''}, + ] }, { url: '/info/', diff --git a/src/routes/document.js b/src/routes/document.js new file mode 100644 index 00000000..47a6bda0 --- /dev/null +++ b/src/routes/document.js @@ -0,0 +1,144 @@ +const React = require('react') +const Share = require('../components/Share') +const DocumentComponent = require('../components/Document') +const Document = require('../core/Document') +const ImageArtifactType = require('../core/ImageArtifactType') +const {render} = require('../lib/render') +const {prioritySort} = require('../lib/sort') +const getPathWithNoTrailingSlash = require('../lib/url').getPathWithNoTrailingSlash +const {get} = require('koa-route') +const {splitBy} = require('../lib/array') +const {unique} = require('../utils/common') + +const sizes = [ + ImageArtifactType.MEDIUM, + ImageArtifactType.BIG, + ImageArtifactType.ORIGIN, +] + +function getMeta(document) { + return { + title: document.title, + description: document.title, + } +} + +async function bakeDocument(document) { + //const preview = await document.preview.unpack() + const preview = document.preview + if (!preview) return null + + const image = preview.findArtifact(sizes) + if (!image) return null + + return { + category: document.category, + title: document.title, + fileName: document.file.name, + fileSize: document.file.size, + fileUrl: document.url, + imageUrl: image.url, + url: document.viewUrl, + } +} + +function getDocument() { + return get('/document/:id', async (ctx, id) => { + const path = getPathWithNoTrailingSlash(ctx.path) + const document = await Document.findById(id) + const data = document + ? await bakeDocument(document) + : null + + if (data) { + const Component = ( +
+ + + +
+ ) + + ctx.type = 'text/html' + ctx.body = await render(path, Component, getMeta(document)) + } else { + ctx.status = 404 + } + }) +} + +function getSorted(documents) { + const compose = (...fns) => value => fns + .map(fn => fn) + .reverse() + .reduce((acc, fn) => fn(acc), value) + + const pass = fn => value => { + fn(value) + return value + } + + const subtract = (a, b) => a - b + + const uniqueCategories = unique(i => i.category) + + const priority = { + 'Основные документы': [ + 'https://static.shlisselburg.org/art/uploads/ustav-2015.pdf', + 'https://static.shlisselburg.org/art/uploads/obrazovatelnaia-litsenziia-2015.pdf', + 'https://static.shlisselburg.org/art/uploads/svidetelstvo-na-pomeshchenie.pdf', + 'https://static.shlisselburg.org/art/uploads/egriul.pdf', + 'https://static.shlisselburg.org/art/uploads/inn.pdf', + 'https://static.shlisselburg.org/art/uploads/reshenie-o-sozdanii-shkoly.pdf', + 'https://static.shlisselburg.org/art/uploads/kopiia-resheniia-o-naznachenii-rukovoditelia.pdf', + 'https://static.shlisselburg.org/art/uploads/postanovlenie-o-sozdanii-munitsipalnogo-obrazovatelnogo-uchrezhdeniia-kultury-detskaia-khudozhestvennaia-shkola.pdf', + ], + 'Учебные программы': [ + 'https://static.shlisselburg.org/art/uploads/uchebnaia-programma-zhivopis.pdf', + ] + } + + const CATEGORY_PRIORITY = [ + 'Основные документы', + 'Планы ФХД', + 'Самообследование деятельности', + 'Инструкции', + 'Образование', + 'Учебные программы', + 'Платные образовательные услуги', + 'Дополнительные общеразвивающие программы', + 'Документы для поступления', + ] + + const sortBy = fn => (a, b) => [a, b] + .map(fn) + .reduce(subtract) + + const getPriorityFn = (m, list) => value => list.includes(value) + ? list.indexOf(value) + : m + + const safeSelect = (d, store, key) => key in store + ? store[key] + : d + + const categoryPriority = getPriorityFn(10000, CATEGORY_PRIORITY) + + const documentPriority = ({category, fileUrl}) => compose( + getPriorityFn.bind(null, 10000), + safeSelect.bind(null, [], priority) + )(category)(fileUrl) + + const documentsOf = category => documents.filter(i => i.category === category) + const sorted = documents => documents.sort(sortBy(documentPriority)) + const collectDocumentsByCategory = compose(sorted, documentsOf) + + return uniqueCategories(documents) + .sort(sortBy(categoryPriority)) + .reduce((acc, name) => [...acc, { + name, + items: collectDocumentsByCategory(name) + }], []) +} + +exports.getDocument = getDocument diff --git a/src/routes/documents.js b/src/routes/documents.js index a5d390b2..cd56ecce 100644 --- a/src/routes/documents.js +++ b/src/routes/documents.js @@ -1,82 +1,155 @@ -import compose from 'koa-compose' -import route from 'koa-route' -import {index, json, accepts} from './' -import {c} from '../core/db' +const React = require('react') +const Article = require('../components/Article') +const DocumentList = require('../components/DocumentList') +const Document = require('../core/Document') +const ImageArtifactType = require('../core/ImageArtifactType') +const {render} = require('../lib/render') +const {prioritySort} = require('../lib/sort') +const getPathWithNoTrailingSlash = require('../lib/url').getPathWithNoTrailingSlash +const {get} = require('koa-route') +const {splitBy} = require('../lib/array') +const {unique} = require('../utils/common') -async function exists(ctx, id) { - let i = await c('documents').findOne({id: id}) - return Boolean(i) +const sizes = [ + ImageArtifactType.MEDIUM, + ImageArtifactType.BIG, + ImageArtifactType.ORIGIN, +] + +function getDocumentsMeta() { + return { + title: 'Документы', + description: 'Документы Шлиссельбургской ДХШ', + } } -export default function (store) { - return compose([ - docs(store), - documents(), - document(), - type() - ]) +function getMeta(teacher) { + return { + title: teacher.name, + description: teacher.name, + } } -function docs(store) { - return route.get('/docs', async ctx => { - const docs = await store.document.find({}) - ctx.body = docs - }) +async function bakeDocument(document) { + //const preview = await document.preview.unpack() + const preview = document.preview + if (!preview) return null + + const image = preview.findArtifact(sizes) + if (!image) return null + + return { + category: document.category, + title: document.title, + fileName: document.file.name, + fileSize: document.file.size, + fileUrl: document.url, + imageUrl: image.url, + url: document.viewUrl, + } } -function documents() { - return route.get('/documents', json( - async(ctx) => { - let query = {} - ctx.body = await c('documents') - .find(query) - .toArray() - } - )) -} +function getDocuments() { + return get('/documents', async (ctx) => { + const path = getPathWithNoTrailingSlash(ctx.path) + let documents = await Document.find({}) -function document() { - return route.get('/documents/:id', accepts({ - 'text/html': index(exists), - 'text/plain': index(exists), - 'application/json': async(ctx, id) => { - let i = await c('documents').findOne({id: id}) - if (!i) ctx.status = 404 - else { - i = await populateDocPreview(i) - ctx.body = i - } - } - })) -} + if (documents) { + //documents = teachersSorted(documents) + documents = await Promise.all(documents.map(bakeDocument)) + documents = documents.filter(Boolean) -function type() { - return route.get('/documents/type/:type', async(ctx, type) => { - let types = { - 'document': ['documents', {type: 'document'}, populateDocPreview], - 'award': ['awards', {type: 'award'}, populateDocPreview] - } + //const collections = splitBy(d => d.category)(documents) + const collections = getSorted(documents) - if (type in types) { - let [collection, query, mapFn] = types[type] + const Component = ( +
+ {collections.map(({name, items}, index) => ( + + ))} +
+ ) - let docs = await c(collection) - .find(query) - .toArray() - - if (mapFn) { - docs = await Promise.all( - docs.map(mapFn) - ) } - - ctx.body = docs + ctx.type = 'text/html' + ctx.body = await render(path, Component, getDocumentsMeta()) } else { ctx.status = 404 } }) } -async function populateDocPreview(doc) { - doc.preview = await c('images').findOne({_id: doc.preview}) - return doc +function getSorted(documents) { + const compose = (...fns) => value => fns + .map(fn => fn) + .reverse() + .reduce((acc, fn) => fn(acc), value) + + const pass = fn => value => { + fn(value) + return value + } + + const subtract = (a, b) => a - b + + const uniqueCategories = unique(i => i.category) + + const priority = { + 'Основные документы': [ + 'https://static.shlisselburg.org/art/uploads/ustav-2015.pdf', + 'https://static.shlisselburg.org/art/uploads/obrazovatelnaia-litsenziia-2015.pdf', + 'https://static.shlisselburg.org/art/uploads/svidetelstvo-na-pomeshchenie.pdf', + 'https://static.shlisselburg.org/art/uploads/egriul.pdf', + 'https://static.shlisselburg.org/art/uploads/inn.pdf', + 'https://static.shlisselburg.org/art/uploads/reshenie-o-sozdanii-shkoly.pdf', + 'https://static.shlisselburg.org/art/uploads/kopiia-resheniia-o-naznachenii-rukovoditelia.pdf', + 'https://static.shlisselburg.org/art/uploads/postanovlenie-o-sozdanii-munitsipalnogo-obrazovatelnogo-uchrezhdeniia-kultury-detskaia-khudozhestvennaia-shkola.pdf', + ], + 'Учебные программы': [ + 'https://static.shlisselburg.org/art/uploads/uchebnaia-programma-zhivopis.pdf', + ] + } + + const CATEGORY_PRIORITY = [ + 'Основные документы', + 'Планы ФХД', + 'Самообследование деятельности', + 'Инструкции', + 'Образование', + 'Учебные программы', + 'Платные образовательные услуги', + 'Дополнительные общеразвивающие программы', + 'Документы для поступления', + ] + + const sortBy = fn => (a, b) => [a, b] + .map(fn) + .reduce(subtract) + + const getPriorityFn = (m, list) => value => list.includes(value) + ? list.indexOf(value) + : m + + const safeSelect = (d, store, key) => key in store + ? store[key] + : d + + const categoryPriority = getPriorityFn(10000, CATEGORY_PRIORITY) + + const documentPriority = ({category, fileUrl}) => compose( + getPriorityFn.bind(null, 10000), + safeSelect.bind(null, [], priority) + )(category)(fileUrl) + + const documentsOf = category => documents.filter(i => i.category === category) + const sorted = documents => documents.sort(sortBy(documentPriority)) + const collectDocumentsByCategory = compose(sorted, documentsOf) + + return uniqueCategories(documents) + .sort(sortBy(categoryPriority)) + .reduce((acc, name) => [...acc, { + name, + items: collectDocumentsByCategory(name) + }], []) } + +exports.getDocuments = getDocuments diff --git a/src/routes/index.js b/src/routes/index.js index bffd844b..ba2fba90 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -7,14 +7,12 @@ import {index as indexFile} from '../config' import sitemap from './sitemap' import schedule from './schedule' -import documents from './documents' import pages from './pages' export function routes(store) { return compose([ sitemap(), schedule(), - documents(store), pages(), ]) } diff --git a/src/server.js b/src/server.js index e2bbbb67..126ab047 100644 --- a/src/server.js +++ b/src/server.js @@ -25,6 +25,8 @@ const article = require('./routes/articles') const albums = require('./routes/albums') const gallery = require('./routes/gallery') const teachers = require('./routes/teachers') +const documents = require('./routes/documents') +const document = require('./routes/document') const dirPublic = path.join(__dirname, '../public') const dirTemplates = path.join(__dirname, 'templates') @@ -70,6 +72,8 @@ export default function (store) { app.use(albums.getAlbum()) app.use(teachers.getCollective(collectiveOrder)) app.use(teachers.getTeacher()) + app.use(documents.getDocuments()) + app.use(document.getDocument()) app.use(routes(store)) return Server(app.callback()) diff --git a/src/style/components/document-row.scss b/src/style/components/document-row.scss new file mode 100644 index 00000000..fa33b3d0 --- /dev/null +++ b/src/style/components/document-row.scss @@ -0,0 +1,50 @@ +@import "../variables"; + +.document-row { + display: flex; + flex-direction: row; + padding: 0; + margin-bottom: $single-margin; +} + +.document-row__image{ + min-width: 4em; + width: 4em; + height: 4em; + + display: flex; + justify-content: center; + align-items: flex-start; + //align-items: center; + + img { + max-width: 100%; + max-height: 100%; + box-shadow: $white-shadow; + } +} + +.document-row__image--medium{ +} + +.document-row__image--big{ + max-width: 20em; +} + +.document-row__file { + margin-left: $single-margin; + flex-grow: 10; +} + +.document-row__file-info { + width: 25%; + flex-grow: 1; + font-size: $smaller-font-size; + text-align: right; +} + +@media (max-width: $mobile-width) { + .document-row { + margin: 0 $single-margin/2 $single-margin; + } +} diff --git a/src/style/components/document.scss b/src/style/components/document.scss index 2397e76a..b862e218 100644 --- a/src/style/components/document.scss +++ b/src/style/components/document.scss @@ -2,19 +2,22 @@ .document { display: flex; - flex-direction: row; + flex-direction: column; + align-items: center; padding: 0; - margin-bottom: $single-margin; + margin-bottom: $double-margin; } .document__image{ - min-width: 4em; - width: 4em; - height: 4em; + //min-width: 10em; + //width: 20em; + //height: 20em; - display: flex; - justify-content: center; - align-items: flex-start; + margin-bottom: $single-margin; + + //display: flex; + //justify-content: center; + //align-items: flex-start; //align-items: center; img { @@ -32,15 +35,11 @@ } .document__file { - margin-left: $single-margin; - flex-grow: 10; + } .document__file-info { - width: 25%; - flex-grow: 1; font-size: $smaller-font-size; - text-align: right; } @media (max-width: $mobile-width) { diff --git a/src/style/style.scss b/src/style/style.scss index f1baca35..bd1fabeb 100644 --- a/src/style/style.scss +++ b/src/style/style.scss @@ -1,6 +1,7 @@ @import "components/album-collection"; @import "components/gallery-item"; @import "components/document"; +@import "components/document-row"; @import "components/documents-container"; @import "components/teacher-proifile"; @import "components/main-menu";