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}) => (
+
+
+
+
+ {titleCase(title)}
+
+
+
+
+)
+
+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}) => (
+
+)
+
+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";