Merge pull request #39 from tmshv/fix/page-update

Page update
This commit is contained in:
tmshv authored and GitHub committed 2020-03-04 01:08:32 +03:00
commit 8741986bb2
83 files changed
+686 -2025

No files matched your search

@@ -1,59 +0,0 @@
import Article from '../core/Article'
import { findArticles } from '../server/articles'
import { Request, Response } from 'express'
import { encodeArticle } from '../factory/article'
const int = (value: string | string[]) => parseInt(Array.isArray(value)
? value[0]
: value
)
export async function getItem(req: Request, res: Response) {
const id = req.params.slug
const article = await Article.findById(id)
if (article) {
res.json(encodeArticle(article))
} else {
res.status(404)
res.json({
error: `Resource ${id} not found`
})
}
}
export async function getAll(req: Request, res: Response) {
const { query } = req
const page = int(query.page)
const pageSize = int(query.pageSize)
const data = await findArticles(page, pageSize)
if (data) {
res.json({
...data,
items: data.items.map(encodeArticle)
})
} else {
res.status(404)
res.json({
error: `Articles not found`
})
}
}
export async function getUrls(req: Request, res: Response) {
const data = await Article.find({})
if (data) {
const items = data.map(x => `/article/${x.id}`)
res.json({
items
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
+71 -6
View File
@@ -1,20 +1,47 @@
import { pick } from 'lodash'
import { Request, Response } from 'express'
import Page from '../core/Page'
import { dropTrailingSlash, getPathWaterfall } from '../lib/url'
import { dropTrailingSlash, readQueryArray, readQueryInt, readQueryString } from '../lib/url'
import { encodePage } from '../factory/page'
import * as breadcrumb from './breadcrumb'
import { isArrayOk } from '../lib/array'
function getSort(sortBy: string) {
if (!sortBy) {
return null
}
if (sortBy.charAt(0) === '-') {
const key = sortBy.substr(1, sortBy.length)
return {
[key]: -1,
}
}
return {
[sortBy]: 1,
}
}
async function resolveItem(page: Page) {
const breadcrumbs = await breadcrumb.getItems(page.getUrl())
page.setBreadcrumb(breadcrumbs)
return page
}
export async function getItem(req: Request, res: Response) {
const page = dropTrailingSlash(req.query.page)
const breadcrumbs = await breadcrumb.getItems(page)
const resource = await Page.findByUrl(page)
const pageUrl = dropTrailingSlash(req.query.page)
const resource = await Page.findByUrl(pageUrl)
if (resource) {
res.json(encodePage(resource, breadcrumbs))
const page = await resolveItem(resource)
res.json(encodePage(page))
} else {
res.status(404)
res.json({
error: `Resource ${page} not found`
error: `Resource ${pageUrl} not found`
})
}
}
@@ -34,6 +61,44 @@ export async function getAll(req: Request, res: Response) {
}
}
export async function getByTags(req: Request, res: Response) {
const skip = readQueryInt(req, 'skip')
const limit = readQueryInt(req, 'limit')
const sortBy = readQueryString(req, 'sortBy')
const sort = getSort(sortBy)
const fields = readQueryArray(req, 'field')
const tags = readQueryArray(req, 'tag')
if (tags.length === 0) {
res.status(400)
return res.json({
error: `No tags in request`
})
}
const options = { skip, limit, sort }
const models = await Page.find({
tags: { $in: tags },
}, options)
if (models) {
const pages = await Promise.all(models.map(resolveItem))
const encodedItems = pages.map(encodePage)
const items = isArrayOk(fields)
? encodedItems.map(x => pick(x, fields))
: encodedItems
res.json({
items,
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
export async function getUrls(req: Request, res: Response) {
const data = await Page.find({})
@@ -1,18 +0,0 @@
import { findSchedule } from '../server/schedule'
import { Request, Response } from 'express'
import { collection } from '../db'
export async function getItem(req: Request, res: Response) {
const period = req.query.period
const semester = req.query.semester
const data = await findSchedule(collection, period, semester)
if (data) {
res.json(data)
} else {
res.status(404)
res.json({
error: `Schedule ${period}:${semester} not found`
})
}
}
+2 -28
View File
@@ -1,35 +1,9 @@
import Article from '../core/Article'
import Page from '../core/Page'
import { Request, Response } from 'express'
import { unique } from '../lib/array'
export async function getAll(req: Request, res: Response) {
const data = await Article.find({})
const tags = data
.flatMap(x => x.tags)
.filter(x => x.isValid())
const items = unique(tags, t => t.slug)
if (data) {
res.json({
items,
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
export async function findItems(req: Request, res: Response) {
const slug = req.params.slug
const data = await Article.find({
tags: {
}
})
const data = await Page.find({})
const tags = data
.flatMap(x => x.tags)
.filter(x => x.isValid())
-120
View File
@@ -1,120 +0,0 @@
import Data from './Data'
import Image from './Image'
import { find, findOne, total } from '../lib/store'
import { Tag } from './Tag'
const store = () => Data.getStore<Article>('Article')
function make(C) {
return x => new C(x)
}
export default class Article {
static async find(query, options = {}): Promise<Article[]> {
const items = await find(store(), query, options as any)
return Promise.all(items.map(processArticle))
}
static async findById(id) {
const item = await findOne(store(), { id })
return !item
? null
: processArticle(item)
}
static async total(query = {}) {
return total(store(), query)
}
static async findPinned() {
const now = new Date()
const query = { until: { $gte: now } }
return Article.find(query, {
sort: { date: -1 },
})
}
public id: any
public images: any
public post: any
public folder: any
public date: any
public title: any
public until: any
public tags: Tag[]
public content: any
public hash: any
public type: any
public origin: any
public file: any
public version: any
public preview: any
public url: any
public featured: boolean
constructor(data) {
this.id = data.id
this.images = data.images
this.post = data.post
this.folder = data.folder
this.date = data.date
this.title = data.title
this.until = data.until
this.tags = (data.tags || []).map(make(Tag))
this.content = data.content
this.hash = data.hash
this.type = data.type
this.origin = data.origin
this.file = data.file
this.version = data.version
this.preview = data.preview
this.featured = data.featured || false
this.url = `/article/${this.id}`
}
plain() {
return {
id: this.id,
images: this.images,
post: this.post,
folder: this.folder,
date: this.date,
title: this.title,
until: this.until,
tags: this.tags,
content: this.content,
hash: this.hash,
type: this.type,
origin: this.origin,
file: this.file,
version: this.version,
preview: this.preview,
url: this.url,
featured: this.featured,
}
}
}
const previewFromImage = (imgs = []) => imgs.length
? imgs[0]
: null
async function processArticle(article) {
let preview = null
const previewId = article.preview
? article.preview
: previewFromImage(article.images)
if (previewId) {
preview = await Image.findById(previewId)
}
return new Article({
...article,
preview,
})
}
+33 -9
View File
@@ -1,12 +1,27 @@
import Data from './Data'
import ImageArtifactType from './ImageArtifactType'
import { findOne } from '../lib/store'
import { mapOf } from '../server/lib'
import { ObjectId } from 'mongodb'
/**
*
* {a: {}, b: {}, ...} -> Map (a: Class, b: Class)
*
* @param Class
* @param object
*/
function mapOf(Class, object) {
return Object
.entries(object)
.reduce((map, i) => (
map.set(i[0], new Class(i[1]))
), new Map())
}
const store = () => Data.getStore('Image')
export default class Image {
static async findById(id) {
static async findById(id: ObjectId) {
const data = await findOne(store(), { _id: id })
if (!data) return null
@@ -21,9 +36,9 @@ export default class Image {
const artifacts = data.data
return new Image({ ...data, artifacts })
}
public hash: any
public file: any
public artifacts: any
public hash: string
public file: string
public artifacts: Map<string, ImageArtifact>
constructor({ hash, file, artifacts }) {
this.hash = hash
@@ -64,13 +79,22 @@ export default class Image {
set: srcset,
}
}
getSrc(): string | null {
const a = this.getArtifact(ImageArtifactType.ORIGINAL)
if (!a) {
return null
}
return a.url
}
}
class ImageArtifact {
public url: any
public size: any
public width: any
public height: any
public url: string
public size: string
public width: number
public height: number
constructor({ url, size, width, height }) {
this.url = url
+43 -26
View File
@@ -1,9 +1,18 @@
import Data from './Data'
import { find as storeFind, findOne, total } from '../lib/store'
import Image from './Image'
import { Tag } from './Tag'
import { ObjectId } from 'mongodb'
import { Breadcrumb } from '../types'
const store = () => Data.getStore('Page')
type PageDto = {
[name: string]: any
preview?: ObjectId
images: ObjectId[]
}
export default class Page {
static async find(query, options = {}): Promise<Page[]> {
const items = await storeFind(store(), query, options as any)
@@ -19,7 +28,7 @@ export default class Page {
: processPage(item)
}
static async findByUrl(url) {
static async findByUrl(url: string) {
const item = await findOne(store(), { url })
return !item
@@ -37,14 +46,19 @@ export default class Page {
public url: string
public content: any
public title: any
public date: Date
public tags: Tag[]
public data: any
public images: any
public images: ObjectId[]
public documents: any
public preview: any
public preview?: Image
public tokens: any[]
public description: string
public featured: boolean
constructor(data) {
private breadcrumb: Breadcrumb
constructor(data: { [name: string]: any }) {
this.id = data.id
this.hash = data.hash
this.file = data.file
@@ -52,45 +66,48 @@ export default class Page {
this.content = data.content
this.title = data.title
this.data = data.data
this.date = data.date
this.images = data.images
this.documents = data.documents
this.preview = data.preview
this.tokens = data.tokens
this.tags = (data.tags || []).map((x: string) => new Tag(x, x))
this.featured = data.featured
this.description = data.description
}
plain() {
return {
id: this.id,
images: this.images,
documents: this.documents,
data: this.data,
title: this.title,
content: this.content,
hash: this.hash,
file: this.file,
preview: this.preview,
url: this.url,
}
public setBreadcrumb(value: Breadcrumb) {
this.breadcrumb = value
return this
}
public getBreadcrumb() {
return this.breadcrumb
}
public getUrl() {
return this.url
}
}
const previewFromImage = (imgs = []) => imgs.length
? imgs[0]
: null
function firstItem<T>(items: T[] = []): T {
return items.length
? items[0]
: null
}
async function processPage(article) {
let preview = null
const previewId = article.preview
? article.preview
: previewFromImage(article.images)
async function processPage(item: PageDto): Promise<Page> {
let preview: Image = null
const previewId = item.preview
? item.preview
: firstItem(item.images)
if (previewId) {
preview = await Image.findById(previewId)
}
return new Page({
...article,
...item,
preview,
})
}
+11 -6
View File
@@ -1,15 +1,20 @@
import { translit } from '../lib/translit'
export class Tag {
public static fromCyrillic(tag: string) {
const slug = tag
? translit(tag).toLowerCase()
: tag
return new Tag(tag, slug)
}
public name: string
public slug: string
constructor(tag: string) {
this.name = tag
if (tag) {
this.slug = translit(tag).toLowerCase()
}
constructor(name: string, slug: string) {
this.name = name
this.slug = slug
}
public isValid() {
-1
View File
@@ -27,7 +27,6 @@ function init(db: Db) {
console.log('INIT DB')
Data.setStore('Page', db.collection('pages'))
Data.setStore('Article', db.collection('articles'))
Data.setStore('Image', db.collection('images'))
Data.setStore('Document', db.collection('documents'))
Data.setStore('Settings', db.collection('settings'))
@@ -1,11 +0,0 @@
import Article from '../core/Article'
import { encodeImage } from './image'
export function encodeArticle(article: Article) {
const preview = article.preview ? encodeImage(article.preview) : null
return {
...article,
preview,
}
}
+27 -3
View File
@@ -1,6 +1,30 @@
import ImageArtifactType from '../core/ImageArtifactType'
import Image from '../core/Image'
export function encodeImage(image) {
export type ImageArtifactDto = {
height: number
width: number
src: string
set: Array<{
density: number
url: string
}>
}
export type ImageDto = {
artifacts: {
original: ImageArtifactDto,
large: ImageArtifactDto,
big: ImageArtifactDto,
medium: ImageArtifactDto,
small: ImageArtifactDto,
fb: ImageArtifactDto,
},
hash: string
file: string
}
export function encodeImage(image: Image): ImageDto {
const artifacts = [
ImageArtifactType.SMALL,
ImageArtifactType.MEDIUM,
@@ -17,8 +41,8 @@ export function encodeImage(image) {
}, {})
return {
artifacts,
artifacts: artifacts as any,
hash: image.hash,
file: image.file,
file: image.file,
}
}
+32 -3
View File
@@ -1,9 +1,34 @@
import { encodeImage } from './image'
import { encodeImage, ImageDto } from './image'
import Page from '../core/Page'
import { Breadcrumb } from '../types'
import { Tag } from '../core/Tag'
export function encodePage(page: Page, breadcrumb: Breadcrumb) {
type Token = {
token: string
data: any
}
type PageDto = {
id: string
title: string
date?: string
url: string
data: string
description: string
preview?: ImageDto
tokens: Token[]
tags: Tag[]
featured: boolean
breadcrumb: Breadcrumb
coverSrc?: string
}
export function encodePage(page: Page): PageDto {
const preview = page.preview ? encodeImage(page.preview) : null
const date = page.date ? page.date.toString() : null
const breadcrumb = page.getBreadcrumb()
const coverSrc = page.preview ? page.preview.getSrc() : null
return {
id: page.id,
@@ -11,8 +36,12 @@ export function encodePage(page: Page, breadcrumb: Breadcrumb) {
url: page.url,
data: page.data,
description: page.description,
preview,
tokens: page.tokens,
tags: page.tags,
featured: page.featured,
preview,
date,
breadcrumb,
coverSrc,
}
}
+27 -1
View File
@@ -1,5 +1,31 @@
export function unique<T>(items: T[], key: (item: T) => string): T[]{
export function unique<T>(items: T[], key: (item: T) => string): T[] {
const map = new Map(items.map(x => [key(x), x]))
return [...map.values()]
}
export function ensureArray<T>(value: T | T[]): T[] {
if (!value) {
return []
}
if (Array.isArray(value)) {
return value
}
return [value]
}
/**
*
* Check if array has one item at least
*
* @param items
*/
export function isArrayOk<T>(items: T[]): boolean {
if (!Array.isArray(items)) {
return false
}
return items.length > 0
}
-7
View File
@@ -1,7 +0,0 @@
/**
*
* Date -> Unix time
*
* @param date
*/
export const timestamp = (date: Date) => date.getTime()
-23
View File
@@ -1,23 +0,0 @@
const getExt = (name: string) => /\.(\w+)$/.exec(name)[1]
const defaultExt = (ext: string) => ext.toUpperCase()
const exts = ['png', 'pdf', 'jpg', 'jpeg', 'gif', 'doc']
const extd = ['ПНГ', 'ПДФ', 'ЖПГ', 'ЖПГ', 'ГИФ', 'ДОК']
const units = ['байт', 'КБ', 'МБ', 'ГБ', 'ТБ', 'ПБ']
export function ext(filname: string): string {
const ext = getExt(filname)
const i = exts.indexOf(ext)
return i > -1
? extd[i]
: defaultExt(ext)
}
export function size(bytes: any, precision = 1): string {
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return ''
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]}`
}
-22
View File
@@ -1,22 +0,0 @@
function sort(list, fn, reverse = false) {
return list
.map(i => [fn(i), i])
.sort((a, b) => reverse
? a[0] - b[0]
: b[0] - a[0])
.map(i => i[1])
}
function sortBy(fn, reverse = false) {
return (i1, i2) => {
const t1 = fn(i1)
const t2 = fn(i2)
return reverse
? t1 - t2
: t2 - t1
}
}
exports.sortBy = sortBy
exports.sort = sort
+10 -10
View File
@@ -1,19 +1,19 @@
export async function find(collection, query, {sort, skip, limit}) {
const cursor = collection.find(query)
export async function find(collection, query, { sort, skip, limit }) {
const cursor = collection.find(query)
if (sort) cursor.sort(sort)
if (skip) cursor.skip(skip)
if (limit) cursor.limit(limit)
if (sort) cursor.sort(sort)
if (skip) cursor.skip(skip)
if (limit) cursor.limit(limit)
return cursor.toArray()
return cursor.toArray()
}
export async function findOne(collection, query) {
return collection.findOne(query)
return collection.findOne(query)
}
export async function total(collection, query) {
return collection
.find(query)
.count()
return collection
.find(query)
.count()
}
-12
View File
@@ -1,12 +0,0 @@
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
+20
View File
@@ -1,3 +1,6 @@
import { Request } from 'express'
import { ensureArray } from './array'
export function dropTrailingSlash(path: string) {
if (path === '/' || path === '') {
return '/'
@@ -26,3 +29,20 @@ export function getPathWaterfall(path: string): string[] {
return ['/', ...parts]
}
export function readQueryInt(req: Request, key: string) {
const int = (value: string | string[]) => parseInt(Array.isArray(value)
? value[0]
: value
)
return int(req.query[key]) || 0
}
export function readQueryString(req: Request, key: string) {
return `${req.query[key]}`
}
export function readQueryArray(req: Request, key: string) {
return ensureArray(req.query[key])
}
-22
View File
@@ -1,22 +0,0 @@
const courses = {
painting: ['Живопись', ''],
drawing: ['Рисунок', '/courses/drawing'],
sculpting: ['Скульптура', ''],
sculpture: ['Скульптура', ''],
ceramic: ['Керамика', '/courses/ceramics'],
ceramics: ['Керамика', '/courses/ceramics'],
composition: ['Композиция', ''],
'easel-composition': ['Станковая композиция', ''],
dpi: ['ДПИ', ''],
'art-talks': ['Беседа по искусству', ''],
history: ['История искусства', '/courses/art-history'],
cg: ['Комп. графика', '/courses/cg'],
}
export function getCourse(id) {
const course = id in courses ? courses[id] : null
if (!course) return null
const [title, url] = course
return {id, title, url}
}
@@ -1,44 +0,0 @@
export const autumnMonth = 8;
export const springMonth = 0;
export function scheduleDate(schedule){
let {period, semester} = schedule;
let i = semester === 'spring' ? 1 : 0;
let year = getPeriod(period)[i];
return new Date(
year,
i ? springMonth : autumnMonth,
1
);
}
/**
*
* f("2012-2015") -> [2012, 2015]
* f(["2012", "2015"]) -> [2012, 2015]
* f([2012, 2015]) -> [2012, 2015]
*
* @param i
* @returns {*}
*/
export function getPeriod(i){
if (typeof i === 'string') return getPeriod(i.split('-'));
return i.map(Number);
}
/**
*
* f("2012-2015") -> "2012-2015"
* f(["2012", "2015"]) -> "2012-2015"
* f([2012, 2015]) -> "2012-2015"
*
* @param i
* @returns {*}
*/
export function getPeriodString(i) {
return getPeriod(i)
.join('-')
}
+1 -8
View File
@@ -1,8 +1,6 @@
import { config } from 'dotenv'
import express from 'express'
import { connect } from './db'
import * as article from './controllers/article'
import * as schedule from './controllers/schedule'
import * as file from './controllers/file'
import * as page from './controllers/page'
import * as image from './controllers/image'
@@ -23,23 +21,18 @@ async function main() {
}
const app = express()
app.get('/articles', article.getAll)
app.get('/articles/urls', article.getUrls)
app.get('/articles/:slug', article.getItem)
app.get('/files', file.getAll)
app.get('/files/urls', file.getUrls)
app.get('/files/:slug', file.getItem)
app.get('/pages', page.getAll)
app.get('/pages/urls', page.getUrls)
app.get('/pages/tags', page.getByTags)
app.get('/page', page.getItem)
app.get('/image', image.getItem)
app.get('/schedule', schedule.getItem)
app.get('/tags', tag.getAll)
app.get('/tags/:slug', tag.findItems)
app.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`)
@@ -1,56 +0,0 @@
import Article from '../core/Article'
import ImageArtifactType from '../core/ImageArtifactType'
import { sortBy } from '../utils/sort'
import { timestamp } from '../lib/date'
export const sortArticleByDate = sortBy(
x => timestamp(new Date(x.date))
)
export async function findArticlesNin(nin, skip, limit, sort) {
const query = {
_id: { $nin: nin }
}
return Article.find(query, {
sort,
skip,
limit,
})
}
export async function findArticles(page: number, pageSize: number) {
const limit = pageSize
const skip = (page - 1) * pageSize
// const pinnedArticles = page === 1
// ? await Article.findPinned()
// : []
const total = await Article.total()
const totalPages = Math.ceil(total / pageSize)
const id = i => i._id
// const pinnedIds = pinnedArticles.map(id)
// const articles = await findArticlesNin(pinnedIds, skip, limit, { date: -1 })
const articles = await Article.find({}, { skip, limit, sort: { date: -1 } })
const prevPage = page > 1
? page - 1
: null
const nextPage = page < totalPages
? page + 1
: null
const items = [
...articles
]
return {
items,
prevPage,
nextPage,
totalPages,
}
}
-14
View File
@@ -1,14 +0,0 @@
/**
*
* {a: {}, b: {}, ...} -> Map (a: Class, b: Class)
*
* @param Class
* @param object
*/
export function mapOf(Class, object) {
return Object
.entries(object)
.reduce((map, i) => (
map.set(i[0], new Class(i[1]))
), new Map())
}
-134
View File
@@ -1,134 +0,0 @@
import { createMap } from '../utils/common'
import {
scheduleDate,
getPeriod as schedulePeriod,
getPeriodString
} from '../models/schedule'
import { getCourse } from '../models/course'
// import { last } from 'lodash'
// const sortBy = require('../lib/sort').sortBy
const SCHEDULES = 'schedules'
export async function findSchedule(collection, period, semester) {
const schedule = await collection(SCHEDULES).findOne({
$or: [
{ period: period },
{ period: schedulePeriod(period) }
],
semester: semester
})
return schedule ? migrate(schedule) : null
}
// async function findAllScheduleParams() {
// const schedules = await collection(SCHEDULES)
// .find({}, {
// period: 1,
// semester: 1,
// _id: 0,
// })
// .toArray()
// return schedules
// .map(i => ({
// ...i,
// period: schedulePeriod(i.period),
// }))
// .sort(sortBy(
// i => scheduleDate(i).getTime(),
// true,
// ))
// }
// function text({ semester, period }) {
// const word = semester => [['spring', 'Весна'], ['autumn', 'Осень']]
// .find(i => i[0] === semester)
// [1]
// const periodIndex = semester === 'spring' ? 1 : 0
// const year = period[periodIndex]
// return `${word(semester)} ${year} года`
// }
async function migrate(schedule) {
const versions = {
'1.0': migrate10to20,
'2.0': migrate20to30,
}
const version = schedule.version
const update = versions[version]
return version in versions ? migrate(update(schedule)) : schedule
}
function migrate10to20(schedule) {
const dayNames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
const time = time => time
.map(i => i.split(' '))
const week = days => days.reduce((week, lesson, index) => {
if (lesson) {
const day = dayNames[index]
week[day] = [Object.assign(lesson, {
time: time(lesson.time)
})]
}
return week
}, {})
return Object.assign(schedule, {
version: '2.0',
period: schedulePeriod(schedule.period),
groups: schedule.schedule
.map(group => ({
name: group.group,
time: group.time,
week: week(group.week)
}))
})
}
async function migrate20to30(schedule) {
const days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat']//, 'sun']
const persons = []
const person = createMap(i => i.id, persons)
const week = week => days
.map(day => week[day] || [])
.map(lessons => lessons.map(lesson))
const lesson = i => ({
time: i.time,
course: getCourse(i.lesson),
teacher: person.has(i.teacher) ? teacher(i.teacher) : teacherDummy(i.teacher)
})
const teacher = id => ({
id: id,
url: `/teacher/${id}`,
name: personName(id)
})
const teacherDummy = name => ({
id: name,
url: null,
name: name
})
const personName = id => person.has(id) ? person.get(id).name : null
return Object.assign(schedule, {
version: '3.0',
groups: schedule.groups
.map(group => ({
name: group.name,
time: group.time,
week: week(group.week)
}))
})
}
-106
View File
@@ -1,106 +0,0 @@
/**
* Generates list of numbers
* @param {Number} start starting value
* @param {Number} num amount of numbers in list
* @param {Number} step difference value
* @return {Array} list of numbers
*/
export function range(start, num, step) {
var list = [];
var val = start;
for (var i = 0; i < num; i++) {
list.push(val);
val += step;
}
return list;
}
/**
* Return a closure that compares a Number argument with specified Number
* @param withNumber Number to compare with
* @returns {Function} (i) => {}
*/
export function numberEquals(withNumber) {
return i => i === withNumber;
}
/**
* Return a closure that compares a second Number argument with specified Number
* @param withNumber Number to compare with
* @returns {Function} (i) => {}
*/
export function indexEquals(withNumber) {
return (_, i) => i === withNumber;
}
/**
* Return a reduce function
* [['spring', 'Весена'], ['autumn', 'Осень']] -> 'Весна' or 'Осень' depend on byCondition result
* @param byCondition
* @param map
*/
export function select(byCondition, map = i => i) {
return (result, item) => byCondition(item) ? map(item) : map(result)
}
/**
* Select an item in Array by specified selector function
* @param scope
* @param selector
* @returns {*}
*/
export function choise(scope, selector) {
return scope.reduce(selector, null);
}
export function unique<T, U>(fn: (value: T) => U): (list: T[]) => U[] {
return list => Array.from(
new Set(list.map(fn))
)
}
export function branch(fn) {
let keys = unique(fn);
return list => keys(list)
.reduce((dict: any, key: any) => {
dict[key] = list.filter(
i => fn(i) === key
);
return dict;
}, {})
}
export function assign(dict, key, value) {
dict[key] = value;
return dict;
}
export function map(v, fn) {
return value => fn(value) ? v : value;
}
/**
*
* f(g, [x, y, z, ...]) => map of {g(x): x, g(y): y, g(z): z}
*
* @param list
* @param selector
* @returns {*}
*/
export function createMap(selector, list) {
return list.reduce((map, i) => map.set(selector(i), i), new Map());
}
/**
*
* f(g) => x ? g(x) : null
*
* @param {function} fn
* @returns {function(*=): null}
*/
export function nullSafe(fn) {
return i => i ? fn(i) : null;
}
-21
View File
@@ -1,21 +0,0 @@
import {range} from './common';
/**
*
* @param start
* @returns {*}
*/
export function getDates(start) {
const oneDay = (1000 * 60 * 60 * 24);
const startTime = start.getTime();
const startDay = start.getDay();
return range(0, 6, 1)
.reduce((dates, weekDay) => {
let delta = 1 + weekDay - startDay;
let time = startTime + (oneDay * delta);
return dates.concat([new Date(time)]);
}, [])
.map(i => i.getDate());
}
-13
View File
@@ -1,13 +0,0 @@
/**
*
* f([x, y, z, ...]) -> f(v) -> {v.x, v.y, v.z}
*
* @param fields
* @returns {function(*): *}
*/
export function select(fields=[]){
return i => fields.reduce((out, key) => {
out[key] = i[key];
return out;
}, {});
}
-8
View File
@@ -1,8 +0,0 @@
export function query(list) {
return list
.reduce(
(query, param) => query.concat([`${param[0]}=${param[1]}`]),
[]
)
.join('&');
}
-35
View File
@@ -1,35 +0,0 @@
export function pop(data){
var Populator = function(data){
this.target = data;
}
Populator.prototype = {
populate: function(p){
var self = this;
if(typeof p == "function"){
self.target = p(self.target);
}else if(p instanceof Array){
p.forEach(function(i){
self.target = self.populate(i).result();
});
}
return self;
},
result: function(){
return this.target;
}
};
var fun = new Populator(data);
return fun;
}
export function populate(fn, key){
return (dict) => {
let id = dict[key];
let value = fn(id);
dict[key] = value ? value : id;
return dict;
}
}
-26
View File
@@ -1,26 +0,0 @@
export function sortByPattern(list, pattern, map) {
let weight = i => {
const index = pattern.indexOf(map(i))
return index > -1 ? pattern.length - index : 0
}
return sortByWeight(list, weight)
}
export function sortByWeight(list, fn) {
return list
.map(i => [fn(i), i])
.sort((a, b) => b[0] - a[0])
.map(i => i[1])
}
export function sortBy(fn) {
return (i1, i2) => {
let t1 = fn(i1)
let t2 = fn(i2)
if (t1 < t2) return 1
if (t1 > t2) return -1
return 0
}
}
-22
View File
@@ -1,22 +0,0 @@
import {map} from './common';
/**
* Return a function that accept one String parameter and returns result of replacing.
*
* @param searchValue
* @param replaceValue
*/
export const stringReplace = (searchValue, replaceValue) =>
str => str.replace(searchValue, replaceValue);
let toTrue = map(true, i => i === 'true');
let toFalse = map(false, i => i === 'false');
const boolProcessors = [toTrue, toFalse];
export function stringToBoolean(string){
return boolProcessors
.reduce(
(value, fn) => fn(value),
string
);
}
-3
View File
@@ -78,10 +78,8 @@ origin = value('origin')
collection_images = 'images'
collection_documents = 'documents'
collection_articles = 'articles'
collection_pages = 'pages'
hash_salt_articles = '8'
hash_salt_pages = '4'
hash_salt_documents = '4'
hash_salt_images = '5'
@@ -91,7 +89,6 @@ provider_root = value('provider')['root'] # env('SYNC_PROVIDER', 'fs')
f = abs_fn(provider_root)
dir_articles = './Articles'
dir_pages = './Pages'
document_url_template = 'https://static.shlisselburg.org/art/uploads/{file}'
-9
View File
@@ -6,7 +6,6 @@ from sync.data.yandexdisk import YDProvider
import asyncio
import logging
from sync.models.Article import Article
from sync.models.Page import Page
@@ -46,17 +45,9 @@ async def run(run_interval=0):
)
sync_pages.set_options(True, True, True)
sync_articles = Sync(
provider=io(settings.provider_root),
model=Article,
)
sync_articles.set_options(True, True, True)
sync_articles.validate_urls = False
while True:
await asyncio.wait([
run_sync(sync_pages),
run_sync(sync_articles),
])
if run_interval == 0:
-34
View File
@@ -45,12 +45,6 @@ def create_date(date_str, date_formats=None):
return None
# 2016 На крыльях бабочек
# 2016.10 На крыльях бабочек
# 2016.10.10 На крыльях бабочек
folder_name_pattern = re.compile('([\d.]+)(.*)')
async def untouched(items: [Model]) -> [Model]:
if settings.skip_unchanged:
return items
@@ -73,21 +67,6 @@ async def untouched(items: [Model]) -> [Model]:
# return filtered_documents
def create_date_and_title_from_folder_name(folder_name, date_formats=None):
m = folder_name_pattern.findall(folder_name)
if not m:
return None, None
date_str, title = m[0]
title = os.path.splitext(title.strip())[0]
date = create_date(date_str, date_formats)
if not date:
return None, title
return date, title
async def create_post(provider: Provider, folder: str, md: str, sizes):
from kazimir.fix_links_quotes import fix_links_quotes
@@ -171,19 +150,6 @@ async def create_post(provider: Provider, folder: str, md: str, sizes):
return post, images, documents, encoded_tokens
def images_from_html(md):
if not md:
return []
post_html = lxml.html.fromstring(md)
images = []
for img in post_html.cssselect('img'):
src = img.get('src')
images.append(src)
return images
def title_from_html(md):
html = lxml.html.fromstring(md)
titles = html.cssselect('h1')
-7
View File
@@ -5,13 +5,6 @@ from sync import untouched
from sync.data import Provider
def get_id(item):
try:
return item.id
except AttributeError:
return item['id']
class Sync:
def __init__(self, provider: Provider, model):
super().__init__()
-186
View File
@@ -1,186 +0,0 @@
import logging
import os
import kazimir
import settings
from db import collection
from markdown import markdown
from sync import create_date_and_title_from_folder_name, create_date, create_post
from sync.data import list_images
from sync.models import Model
from sync.models.Image import Image
from utils.fn import first, combine
from utils.hash import hash_str
from utils.io import parse_yaml_front_matter
from utils.text.transform import url_encode_text
logger = logging.getLogger(settings.name + '.Article')
store = collection(settings.collection_articles)
class Article(Model):
@staticmethod
async def find(query):
return store.find(query)
@staticmethod
async def delete(query):
return store.find_one_and_delete(query)
@staticmethod
async def scan(provider):
scan_dir = settings.dir_articles
documents = [i for i in provider.scan(scan_dir) if provider.is_dir(i)]
documents = [Article.read(provider, i) for i in documents]
return documents
@staticmethod
def read(provider, path):
file_time_formats = ['%Y.%m.%d']
# PARSE FOLDER NAME -> GET OPTIONAL DATE/TITLE
doc_date, doc_title = create_date_and_title_from_folder_name(path, file_time_formats)
document = {
'folder': path,
'date': doc_date,
'title': doc_title,
}
# TRY TO FILL UP BASIC_MANIFEST WITH MD_MANIFEST
manifest_file = md_from_folder(provider, path)
if manifest_file:
manifest = read_manifest(provider, manifest_file)
else:
manifest = {}
return Article(
provider,
path,
params={
**document,
**manifest,
},
)
def __init__(self, provider, file, params=None):
self.hash_salt = settings.hash_salt_articles
self.version = '2'
self.post = None
self.preview = None
self.images = None
self.documents = None
self.tokens = None
super().__init__(provider, store, file, params=params)
def init(self):
if self.has_param('content'):
images, documents = kazimir.extract_files(self.get_param('content'))
else:
images = list_images(self.provider, self.file)
images = [os.path.relpath(i, self.file) for i in images]
documents = []
self.params['images'] = images
self.params['documents'] = documents
self.__set_id()
self.__set_hash()
self.origin = settings.origin
def validate(self):
document = self.params
if 'date' not in document or not document['date']:
return None
if 'title' not in document or not document['title']:
return None
async def build(self, **kwargs):
sizes = kwargs['sizes']
if self.has_param('content'):
markdown_post = self.get_param('content')
else:
markdown_post = create_post_from_image_list(self.get_param('images'))
post, images, documents, tokens = await create_post(self.provider, self.get_param('folder'), markdown_post, sizes)
self.post = post
self.images = images
self.documents = documents
self.tokens = tokens
if self.has_param('preview'):
preview = self.get_param('preview')
preview = self._get_relpath(preview)
self.preview = await Image.new(self.provider, preview, sizes)
def bake(self):
return {
**self.params,
'images': [x.ref for x in self.images],
'documents': [x.ref for x in self.documents],
'post': self.post,
'id': self.id,
'hash': self.hash,
'file': self.file,
'origin': self.origin,
'version': self.version,
'tokens': self.tokens,
'preview': self.preview.ref if self.preview else None,
}
def __set_id(self):
if 'id' in self.params:
self.id = self.params['id']
else:
self.id = url_encode_text(self.params['title'])
def __set_hash(self):
files = []
files += sorted(self.get_param('images'))
files += sorted(self.get_param('documents'))
if self.has_param('preview'):
files.append(self.get_param('preview'))
folder = self.get_param('folder')
files = [os.path.join(folder, x) for x in files]
files = [self.provider.hash(x) for x in files]
self.hash = hash_str(
combine([self.hash_salt] + [hash_str(self.params)] + files)
)
def __str__(self):
return f'<Article file={self.file} id={self.id}>'
def md_from_folder(provider, i):
return first(provider.type_filter(i, '.md'))
def read_manifest(provider, path):
data = provider.read(path).read().decode('utf-8')
manifest = parse_yaml_front_matter(data)
if 'date' in manifest:
manifest['date'] = create_date(manifest['date'], settings.date_formats)
if 'until' in manifest:
manifest['until'] = create_date(manifest['until'], settings.date_formats)
if 'id' in manifest:
manifest['id'] = str(manifest['id'])
return {
**manifest,
}
def create_post_from_image_list(images):
return markdown(
'\n'.join(map(
lambda i: '![]({img})'.format(img=i),
images
))
)
+2 -8
View File
@@ -11,7 +11,7 @@ from db import collection
from sync.data import request
from sync.models import Model
from sync import create_date, images_from_html, title_from_html, create_post
from sync import create_date, title_from_html, create_post
from sync.models.Image import Image
from utils.hash import hash_str
from utils.io import parse_yaml_front_matter
@@ -64,6 +64,7 @@ class Page(Model):
manifest_path = manifest_file('.md')
if not provider.exists(manifest_path):
logger.warning(f'Manifest not found on path {manifest_path}')
return None
params = {
@@ -173,9 +174,6 @@ class Page(Model):
files = []
files += self.get_param('images')
files += self.get_param('documents')
if self.has_param('contentFile'):
files += [self.get_param('contentFile')]
folder = self.get_param('folder')
files = [os.path.join(folder, x) for x in files]
files = sorted([self.file] + files)
@@ -200,8 +198,4 @@ def get_manifest(provider, path):
if 'id' in manifest:
manifest['id'] = str(manifest['id'])
if 'contentFile' in manifest:
dirname = os.path.dirname(path)
manifest['contentHtml'] = provider.read(os.path.join(dirname, manifest['contentFile'])).read().decode('utf-8')
return manifest
-11
View File
@@ -1,11 +0,0 @@
import os
import re
import settings
__author__ = 'Roman Timashev'
__check_description_filename_exp = re.compile('^[%s]+(\[\d+\])\.md$' % settings.description_available_chars)
def check_description_filename(filename):
return __check_description_filename_exp.match(filename)
+5 -5
View File
@@ -3102,6 +3102,11 @@
}
}
},
"date-fns": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.10.0.tgz",
"integrity": "sha512-EhfEKevYGWhWlZbNeplfhIU/+N+x0iCIx7VzKlXma2EdQyznVlZhCptXUY+BegNpPW2kjdx15Rvq503YcXXrcA=="
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
@@ -6338,11 +6343,6 @@
}
}
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
+1 -1
View File
@@ -21,8 +21,8 @@
"dependencies": {
"axios": "^0.19.0",
"classnames": "^2.2.6",
"date-fns": "^2.10.0",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"next": "^9.2.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
+7 -3
View File
@@ -6,7 +6,7 @@ import { Meta } from 'src/components/Meta'
import { MetaBuilder } from 'src/lib/meta'
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
import { NextPage } from 'next'
import { IBreadcumbsPart, IMeta, IPage } from 'src/types'
import { IBreadcumbsPart, IMeta, IPage, ITag } from 'src/types'
function array<T>(value: T | T[]) {
return Array.isArray(value)
@@ -16,6 +16,8 @@ function array<T>(value: T | T[]) {
type Props = {
title: string
tags: ITag[]
date: string
content: string
breadcrumb: IBreadcumbsPart[]
meta?: IMeta
@@ -23,7 +25,6 @@ type Props = {
const Index: NextPage<Props> = props => (
<App
showAuthor={true}
contentStyle={{
marginTop: 'var(--size-l)'
}}
@@ -37,7 +38,8 @@ const Index: NextPage<Props> = props => (
</Head>
<Page
showSocialShare={true}
tags={props.tags}
date={props.date ? new Date(props.date) : null}
>
{props.content}
</Page>
@@ -68,6 +70,8 @@ export const unstable_getStaticProps = async (ctx: any) => {
props: {
content: page.data,
title: page.title,
tags: page.tags,
date: page.date,
meta,
breadcrumb,
}
-85
View File
@@ -1,85 +0,0 @@
import Head from 'next/head'
import { App } from 'src/components/App'
import { Article } from 'src/components/Article'
import { Html } from 'src/components/Html'
import { Meta } from 'src/components/Meta'
import { MetaBuilder } from 'src/lib/meta'
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
import { NextPage } from 'next'
import { IArticle, IMeta } from 'src/types'
type Props = {
title: string
meta: IMeta
data: IArticle
}
const Page: NextPage<Props> = props => {
if (!props.data) {
console.log('kek error', props)
return null
}
return (
<App
showAuthor={true}
>
<Head>
<title>{props.title}</title>
<Meta meta={props.meta} />
</Head>
<Article
title={props.title}
date={props.data.date}
tags={props.data.tags}
shareable={true}
>
<Html
html={props.data.post}
/>
</Article>
</App>
)
}
export const unstable_getStaticProps = async (ctx: any) => {
const id = ctx.params.slug
const data = await requestGet<IArticle>(createApiUrl(`/api/articles/${id}`), null)
if (!data) {
return null
}
const meta = (new MetaBuilder())
.setImage(data.preview)
.setTitle(data.title)
.build()
return {
props: {
title: data.title,
data,
meta,
}
}
}
export const unstable_getStaticPaths = async () => {
const urls = await requestGet<IResponseItems<string>>(createApiUrl(`/api/articles/urls`), null)
if (!urls) {
return null
}
return {
paths: urls.items
.map(x => x.replace('/article/', ''))
.map(slug => {
return {
params: {
slug,
},
}
}),
}
}
export default Page
-93
View File
@@ -1,93 +0,0 @@
import Head from 'next/head'
import { range } from 'lodash'
import { App } from 'src/components/App'
import { ArticleGrid } from 'src/components/ArticleGrid'
import { MetaBuilder } from 'src/lib/meta'
import { Meta } from 'src/components/Meta'
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
import { NextPage } from 'next'
import { IArticle } from 'src/types'
const pageSize = Number(process.env.APP_ARTICLES_PAGE_SIZE)
type Props = {
meta: any
title: string
data: IArticle[]
nextPage: number
prevPage: number
}
const Page: NextPage<Props> = props => {
if (!props.data) {
console.log('kek error', props)
return null
}
return (
<App
showAuthor={true}
wide={true}
>
<Head>
<title>{props.title}</title>
<Meta meta={props.meta} />
</Head>
<ArticleGrid
articles={props.data}
prevPage={props.prevPage}
nextPage={props.nextPage}
/>
</App>
)
}
export const unstable_getStaticProps = async (ctx: any) => {
const page = parseInt(ctx.params.slug)
const res = await requestGet<IResponseItems<IArticle>>(createApiUrl(`/api/articles?page=${page}&pageSize=${pageSize}`), null)
if (!res) {
return null
}
const data = (res.items || [])
const nextPage = res.nextPage
const prevPage = res.prevPage
const title = 'События'
const meta = (new MetaBuilder())
.setTitle(title)
.build()
return {
props: {
data,
nextPage,
prevPage,
title,
meta,
}
}
}
export const unstable_getStaticPaths = async () => {
const urls = await requestGet<IResponseItems<string>>(createApiUrl(`/api/articles/urls`), null)
if (!urls) {
return null
}
const count = urls.items.length
const pages = Math.ceil(count / pageSize)
return {
paths: range(1, pages + 1)
.map(i => {
return {
params: {
slug: `${i}`,
},
}
}),
}
}
export default Page
+16 -18
View File
@@ -1,14 +1,15 @@
import Head from 'next/head'
import { App } from 'src/components/App'
import { Meta } from 'src/components/Meta'
import { CollectiveImage } from 'src/components/CollectiveImage'
import { meta } from 'src/lib/meta'
import { IMeta, IPage, ImageDefinition } from 'src/types'
import { IMeta, IPage, ImageSize } from 'src/types'
import { createApiUrl, requestGet } from 'src/next-lib'
import { NextPage, NextPageContext } from 'next'
import { CardGrid } from 'src/components/CardGrid'
import { Card } from 'src/components/Card'
import { Block } from 'src/components/Block'
import { Image } from 'src/components/Image'
import { imageSrcSet } from 'src/lib/image'
const persons = [
'/teacher/mg-timasheva',
@@ -38,7 +39,6 @@ type CardData = {
type Props = {
title: string
image: string
meta: IMeta
data: CardData[]
}
@@ -49,6 +49,9 @@ const Page: NextPage<Props> = props => {
return
}
const src = 'https://art.shlisselburg.org/static/img/collective.jpg'
const srcSet = imageSrcSet(src, [ImageSize.big, ImageSize.large, ImageSize.medium, ImageSize.small])
return (
<App
showAuthor={true}
@@ -59,14 +62,14 @@ const Page: NextPage<Props> = props => {
<Meta meta={props.meta} />
</Head>
{!props.image ? null : (
<CollectiveImage
data={props.image}
style={{
marginBottom: 'var(--size-l)',
}}
/>
)}
<Image
src={src}
srcSet={srcSet}
style={{
marginBottom: 'var(--size-l)',
borderRadius: 'var(--radius)',
}}
/>
<CardGrid style={{
marginBottom: 'var(--size-xl)',
@@ -100,10 +103,6 @@ const Page: NextPage<Props> = props => {
export const unstable_getStaticProps = async (ctx: NextPageContext) => {
const title = 'Преподаватели Шлиссельбургской ДХШ'
const imageFile = 'Images/HudozkaCollective2017.jpg'
const resImage = await requestGet<ImageDefinition>(createApiUrl(`/api/image?file=${imageFile}`), null)
const image = resImage?.artifacts.large
const pages = await Promise.all(persons
.map(page => requestGet<IPage>(createApiUrl(`/api/page?page=${page}`), null))
)
@@ -116,16 +115,15 @@ export const unstable_getStaticProps = async (ctx: NextPageContext) => {
title: x.title,
caption: x.description,
href: x.url,
imageSrc: image.src,
imageAlt: title,
imageSrcSet: image.set.join(' ')
imageSrc: image.src,
imageSrcSet: imageSrcSet(image.src, [ImageSize.medium, ImageSize.small])
}
})
return {
props: {
data,
image,
title,
meta: meta({
title,
+16 -23
View File
@@ -1,22 +1,20 @@
import Head from 'next/head'
import { App } from 'src/components/App'
import { Meta } from 'src/components/Meta'
import { ArticleGrid } from 'src/components/ArticleGrid'
import { PageGrid } from 'src/components/PageGrid'
import { HudozkaTitle } from 'src/components/HudozkaTitle'
import { meta, MetaBuilder } from 'src/lib/meta'
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
import { MetaBuilder } from 'src/lib/meta'
import { getPagesCardsByTags } from 'src/rest'
import { NextPage } from 'next'
import { IArticle } from 'src/types'
import { IMeta, PageCardData } from 'src/types'
interface IProps {
title: string
meta: any
prevPage: number
nextPage: number
articles: any[]
meta: IMeta
items: PageCardData[]
}
const Page: NextPage<IProps> = props => (
const Index: NextPage<IProps> = props => (
<App
showAuthor={true}
wide={true}
@@ -32,21 +30,18 @@ const Page: NextPage<IProps> = props => (
}}
/>
<ArticleGrid
articles={props.articles}
prevPage={props.prevPage}
nextPage={props.nextPage}
<PageGrid
items={props.items}
/>
</App>
)
export const unstable_getStaticProps = async () => {
const page = 1
const pageSize = process.env.APP_ARTICLES_PAGE_SIZE
const res = await requestGet<IResponseItems<IArticle>>(createApiUrl(`/api/articles?page=${page}&pageSize=${pageSize}`), null)
const articles = (res.items || [])
const nextPage = res.nextPage
const prevPage = res.prevPage
const items = await getPagesCardsByTags([
'event',
'album',
'post',
])
const title = 'Шлиссельбургская ДХШ'
const meta = (new MetaBuilder())
.setTitle(title)
@@ -57,13 +52,11 @@ export const unstable_getStaticProps = async () => {
return {
props: {
articles,
nextPage,
prevPage,
items,
title,
meta,
}
}
}
export default Page
export default Index
+7 -4
View File
@@ -6,9 +6,12 @@ const api = axios.create({
baseURL: 'https://api.tmshv.com/hudozka',
})
async function getArticles(pageSize) {
const page = 1
const res = await api.get(`/api/articles?page=${page}&pageSize=${pageSize}`)
async function getArticles(limit) {
const query = ['post', 'event', 'album']
.map(t => `tag=${t}`)
.join('&')
const url = `/api/pages/tags?${query}&sortBy=-date&skip=0&pageSize=${limit}`
const res = await api.get(url)
return res.data.items
}
@@ -32,7 +35,7 @@ async function getArticles(pageSize) {
date: new Date(article.date),
id: article.id,
link: article.url,
content: article.post,
content: article.data,
})
})
-10
View File
@@ -30,20 +30,10 @@ async function getPageUrls(frequency = 'daily') {
}))
}
async function getArticleUrls() {
const res = await api.get('/api/articles/urls')
return res.data.items.map(url => ({
changefreq: 'monthly',
url,
}))
}
(async () => {
let urls = await Promise.all([
getMenuUrls(),
getPageUrls(),
getArticleUrls(),
])
urls = urls.reduce((urls, i) => urls.concat(i))
+3 -3
View File
@@ -15,13 +15,13 @@ const Navigation = dynamic(() => import('../Navigation').then(mod => mod.Navigat
})
export interface IAppProps {
showAuthor: boolean
showAuthor?: boolean
wide?: boolean
contentStyle?: React.CSSProperties
breadcrumbs?: IBreadcumbsPart[]
}
export const App: React.FC<IAppProps> = props => {
export const App: React.FC<IAppProps> = ({ showAuthor = false, ...props }) => {
const hideBreadcrumbs = useMobile()
const router = useRouter()
const wide = props.wide ?? false
@@ -51,7 +51,7 @@ export const App: React.FC<IAppProps> = props => {
)}
footer={(
<Footer
showAuthor={props.showAuthor}
showAuthor={showAuthor}
/>
)}
>
-55
View File
@@ -1,55 +0,0 @@
import { dateFormat } from 'src/lib/date'
import { Share } from '../Share'
import { TagList } from '../TagList'
import { ITag } from 'src/types'
export type ArticleProps = {
date: string
title: string
tags: ITag[]
shareable: boolean
}
type HeadProps = {
date: string
tags: ITag[]
}
const Head: React.FC<HeadProps> = ({ date, tags = [], children }) => (
<header className="Article-Head">
<style jsx>{`
.date {
color: var(--color-text-second);
}
`}</style>
<h1>{children}</h1>
{!date ? null : (
<p className={'date'}>Опубликовано: {dateFormat(date)}</p>
)}
{!tags.length ? null : (
<TagList items={tags} />
)}
</header>
)
export const Article: React.FC<ArticleProps> = ({ children, date, tags, title, shareable }) => (
<article className="Article">
<Head
date={date}
tags={tags}
>
{title}
</Head>
<div className="Article-Body">
{children}
</div>
{!shareable ? null : (
<Share />
)}
</article>
)
-116
View File
@@ -1,116 +0,0 @@
import { IArticle, ImageDefinition } from 'src/types'
import { Pager } from '../Pager'
import { CardGrid } from '../CardGrid'
import { Card, CardLayout } from '../Card'
import { Date } from './Date'
import { Block } from '../Block'
import { Spacer } from '../Spacer'
export interface IResizeImage {
w: number
h: number
}
export function resizeImage(src: string, options: IResizeImage): string {
return `https://images.weserv.nl/?url=${src}&w=${options.w}&h=${options.h}&n=-1`
}
const cardImgSize1 = { w: 500, h: 500 }
const cardImgSize2 = { w: 1000, h: 1000 }
function getImage(image?: ImageDefinition) {
if (!image) {
const defaultSrc = process.env.APP_CARD_DEFAULT_IMAGE
const src = resizeImage(defaultSrc, cardImgSize1)
const set = [
{
density: 2,
href: resizeImage(defaultSrc, cardImgSize2),
}
]
const srcSet = set
.map(({ href, density }) => `${href} ${density}x`)
.join(' ')
return {
src,
srcSet,
}
}
const preview = image.artifacts.medium
const src = preview.src
const srcSet = preview.set.map(({ url, density }) => `${url} ${density}x`).join(' ')
return {
src,
srcSet,
}
}
export type ArticleGridProps = {
articles: IArticle[]
nextPage: number | null
prevPage: number | null
}
export const ArticleGrid: React.FC<ArticleGridProps> = ({ articles, nextPage, prevPage, ...props }) => {
const nextHref = nextPage ? `/articles/${nextPage}` : null
let prevHref = prevPage ? `/articles/${prevPage}` : null
if (prevPage === 1) {
prevHref = '/'
}
return (
<>
<CardGrid
style={{
marginBottom: 'var(--size-m)',
}}
>
{articles.map((article, i) => {
const gridColumn = article.featured ? 'span 2' : 'auto'
const layout: CardLayout = article.featured ? 'featured' : 'simple'
const { src, srcSet } = getImage(article.preview)
const content = !article.featured
? (
<Block
direction={'vertical'}
>
{article.title}
<Spacer />
<Date style={{
marginTop: 'var(--size-m)',
}}>{article.date}</Date>
</Block>
) : (
article.title
)
return (
<Card
key={i}
href={article.url}
img={{
alt: article.title,
src,
srcSet,
}}
layout={layout}
style={{
gridColumn,
}}
>
{content}
</Card>
)
})}
</CardGrid>
<Pager
nextHref={nextHref}
prevHref={prevHref}
/>
</>
)
}
+2 -2
View File
@@ -1,6 +1,7 @@
import './styles.css'
import cx from 'classnames'
import Link from 'next/link'
import { Image } from '../Image'
export type CardLayout = 'simple' | 'featured'
export type CardProps = {
@@ -19,8 +20,7 @@ export const Card: React.FC<CardProps> = props => (
<Link href={props.href}>
<a>
<div className={'card-image'}>
<img
className={cx({ opa: false })}
<Image
alt={props.img.alt}
src={props.img.src}
srcSet={props.img.srcSet}
-22
View File
@@ -1,22 +0,0 @@
import React from 'react'
import { Image } from './Image'
export const CollectiveImage = (props) => (
<div
style={props.style}
>
<Image
src={props.data.src}
alt={props.data.alt}
set={props.data.set}
opa={false}
style={{
marginBottom: 'var(--double-margin)',
}}
imgStyle={{
width: '100%',
borderRadius: 'var(--radius)',
}}
/>
</div>
)
+26 -31
View File
@@ -1,40 +1,35 @@
import * as React from 'react'
import cx from 'classnames'
import './styles.css'
import { memo, useRef, useEffect, useCallback } from 'react'
export interface IImageProps {
export type ImageProps = {
style?: React.CSSProperties
imgStyle?: React.CSSProperties
src: string
alt: string
opa: boolean
set: Array<{
url: string
density: number
}>
alt?: string
srcSet?: string
}
export const Image: React.FC<IImageProps> = React.memo(props => {
const srcSet = props.set
.map(({ url, density }) => `${url} ${density}x`)
.join(' ')
export const Image: React.FC<ImageProps> = memo(props => {
const ref = useRef<HTMLImageElement>()
const onLoad = useCallback((event) => {
event.target.classList.add('loaded')
event.target.classList.remove('pending')
}, [])
useEffect(() => {
if (ref.current) {
ref.current.classList.add('pending')
}
})
return (
<picture style={props.style}>
<style jsx>{`
picture img {
display: block;
}
`}</style>
<img
style={props.imgStyle}
className={cx({
opa: props.opa
})}
alt={props.alt}
src={props.src}
srcSet={srcSet}
/>
</picture >
<img
ref={ref}
onLoad={onLoad}
className={'image'}
style={props.style}
alt={props.alt}
src={props.src}
srcSet={props.srcSet}
/>
)
})
+12
View File
@@ -0,0 +1,12 @@
.image {
width: 100%;
display: block;
}
.image.pending {
/* opacity: 0; */
}
.image.loaded {
/* opacity: 1; */
}
+21 -4
View File
@@ -1,21 +1,38 @@
import { Share } from '../Share'
import { Html } from '../Html'
import { PageMeta } from '../PageMeta'
import { ITag } from 'src/types'
export interface IPageProps {
showSocialShare: boolean
children: string
date: Date
tags: ITag[]
}
export const Page: React.FC<IPageProps> = props => (
<div className={'Article'}>
<style jsx>{`
hr {
border: 1px solid black;
margin-top: var(--size-m);
margin-bottom: var(--size-m);
}
`}</style>
<div className={'Article-Body'}>
<Html
html={props.children}
/>
</div>
{!props.showSocialShare ? null : (
<Share />
)}
<hr />
<Share />
<PageMeta
style={{
marginTop: 'var(--size-m)',
}}
date={props.date}
tags={props.tags}
/>
</div>
)
File renamed without changes.
+87
View File
@@ -0,0 +1,87 @@
import { ImageSize, PageCardData } from 'src/types'
import { CardGrid } from '../CardGrid'
import { Card, CardLayout } from '../Card'
import { Date } from './Date'
import { Block } from '../Block'
import { Spacer } from '../Spacer'
import { imageSrcSet, imageSrc } from 'src/lib/image'
import { Button } from '../Button'
import { useState, useCallback } from 'react'
export type PageGridProps = {
items: PageCardData[]
}
export const PageGrid: React.FC<PageGridProps> = props => {
const [page, setPage] = useState(1)
const i = page * 24
const items = props.items.slice(0, i)
const onMore = useCallback(() => {
setPage(x => x + 1)
}, [])
const hasMore = i < props.items.length
return (
<>
<CardGrid
style={{
marginBottom: 'var(--size-m)',
}}
>
{items.map((item, i) => {
const gridColumn = item.featured ? 'span 2' : 'auto'
const layout: CardLayout = item.featured ? 'featured' : 'simple'
const cover = item.coverSrc ?? process.env.APP_CARD_DEFAULT_IMAGE
const src = cover
const srcSet = imageSrcSet(src, [ImageSize.large, ImageSize.medium, ImageSize.small])
const content = !item.featured
? (
<Block
direction={'vertical'}
>
{item.title}
<Spacer />
<Date style={{
marginTop: 'var(--size-m)',
}}>{item.date}</Date>
</Block>
) : (
item.title
)
return (
<Card
key={i}
href={item.url}
img={{
alt: item.title,
src,
srcSet,
}}
layout={layout}
style={{
gridColumn,
}}
>
{content}
</Card>
)
})}
</CardGrid>
<Block direction={'horizontal'} style={{
marginTop: 'var(--size-l)'
}}>
<Spacer />
<Button
disabled={!hasMore}
onClick={onMore}
style={{
flex: 2,
}}
>Загрузить ещё</Button>
<Spacer />
</Block>
</>
)
}
-22
View File
@@ -1,22 +0,0 @@
import * as React from 'react'
import { dateFormat } from '../../lib/date'
export interface IPageHeaderProps {
tags?: string[]
date?: Date
title: string
}
export const PageHeader: React.FC<IPageHeaderProps> = props => (
<header className="Article-Head">
<h1>{props.title}</h1>
{!props.date ? null : (
<p className="date">{dateFormat(props.date)}</p>
)}
{/* {!(tags.length) ? null : (
<TagList tags={tags} />
)} */}
</header>
)
+34
View File
@@ -0,0 +1,34 @@
import './styles.css'
import cx from 'classnames'
import { dateFormat } from 'src/lib/date'
import { ITag } from 'src/types'
import { TagList, Direction } from '../TagList'
import { useMobile } from 'src/hooks/useMobile'
export type PageMetaProps = {
style?: React.CSSProperties
date?: Date
tags?: ITag[]
}
export const PageMeta: React.FC<PageMetaProps> = props => {
const mobile = useMobile()
const center = mobile ? null : 'center'
const direction: Direction = mobile ? 'vertical' : 'horizontal'
return (
<div className={cx('pageMeta', center)} style={props.style}>
{!props.date ? null : (
<p className={'date'}>Опубликовано {dateFormat(props.date)}</p>
)}
{!(props.tags?.length) ? null : (
<TagList
direction={direction}
items={props.tags}
/>
)}
</div>
)
}
+8
View File
@@ -0,0 +1,8 @@
.pageMeta {
display: flex;
justify-content: space-between;
}
.pageMeta.center {
align-items: center;
}
+21 -5
View File
@@ -1,26 +1,42 @@
import './styles.css'
import cx from 'classnames'
import { ITag } from 'src/types'
import { Button } from '../Button'
export const Tag: React.FC<{ href: string }> = props => (
<li className={'tagsItem'}>
<Button href={props.href}>{props.children}</Button>
export type Direction = 'horizontal' | 'vertical'
export type TagProps = {
href: string
direction: Direction
}
export const Tag: React.FC<TagProps> = props => (
<li className={cx('tagsItem', props.direction)}>
<Button
href={props.href}
size={'small'}
disabled={true}
>
{props.children}
</Button>
</li>
)
export type TagListProps = {
items: ITag[]
direction: Direction
}
export const TagList: React.FC<TagListProps> = props => (
<ul className={'tags'}>
<ul className={cx('tags', props.direction)}>
{props.items.map(x => (
<Tag
key={x.slug}
href={x.href}
direction={props.direction}
>
# {x.name}
{x.name}
</Tag>
))}
</ul>
+13
View File
@@ -8,10 +8,23 @@
list-style: none;
}
.tags.vertical {
flex-direction: column;
}
.tagsItem {
margin-right: var(--size-s);
margin-bottom: var(--size-s);
}
.tagsItem:last-child {
margin-right: 0;
}
.tagsItem.vertical {
margin-bottom: var(--size-s);
}
.tagsItem.tags.vertical:last-child {
margin-bottom: 0;
}
-36
View File
@@ -1,36 +0,0 @@
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
+9 -12
View File
@@ -1,18 +1,15 @@
import moment from 'moment'
moment.locale('ru')
import format from 'date-fns/format'
import { ru } from 'date-fns/locale'
/**
*
* Date -> DD.MM.YYYY
* Date -> D MMMM yyyy
*
* @param date
*/
export const dateFormat = (date: string | Date) => moment(date).format('L')
/**
*
* Date -> Unix time
*
* @param date
*/
export const timestamp = (date: Date) => date.getTime()
export const dateFormat = (date: string | Date) => {
const value = typeof date === 'string' ? new Date(date) : date
return format(value, "d MMMM yyyy", {
locale: ru
})
}
+36
View File
@@ -0,0 +1,36 @@
import { ImageSize } from 'src/types'
export type ResizeOptions = {
width: number
height: number
}
export function getResizedUrl(src: string, options: ResizeOptions): string {
if (!src || src === '') {
return null
}
return `https://images.weserv.nl/?url=${src}&w=${options.width}&h=${options.height}&n=-1`
}
export function imageSrcSet(src: string, sizes: ImageSize[]): string {
const set = sizes
.map(x => ({
value: x,
href: getResizedUrl(src, {
width: x,
height: x,
}),
}))
return set
.map(x => `${x.href} ${x.value}w`)
.join(', ')
}
export function imageSrc(src: string, size: ImageSize): string {
return getResizedUrl(src, {
width: size,
height: size,
})
}
-37
View File
@@ -1,37 +0,0 @@
function prioritySort(priority, key, list) {
priority = priority.reverse()
let weight = i => {
const p = key(i)
const index = priority.indexOf(p)
return index > -1
? index + 1
: 0
}
return sort(list, weight)
}
function sort(list, fn, reverse = false) {
return list
.map(i => [fn(i), i])
.sort((a, b) => reverse
? a[0] - b[0]
: b[0] - a[0])
.map(i => i[1])
}
function sortBy(fn, reverse = false) {
return (i1, i2) => {
const t1 = fn(i1)
const t2 = fn(i2)
return reverse
? t1 - t2
: t2 - t1
}
}
exports.prioritySort = prioritySort
exports.sortBy = sortBy
exports.sort = sort
-19
View File
@@ -1,19 +0,0 @@
export async function find(collection, query, {sort, skip, limit}) {
const cursor = collection.find(query)
if (sort) cursor.sort(sort)
if (skip) cursor.skip(skip)
if (limit) cursor.limit(limit)
return cursor.toArray()
}
export async function findOne(collection, query) {
return collection.findOne(query)
}
export async function total(collection, query) {
return collection
.find(query)
.count()
}
-12
View File
@@ -1,12 +0,0 @@
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
+3
View File
@@ -0,0 +1,3 @@
export function titleCase(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
+10
View File
@@ -1,3 +1,13 @@
export function isPartOfPath(part: string, path: string): boolean {
return path.startsWith(part)
}
export function queryList(key: string, values: string[]): string {
if (values.length === 0) {
return ''
}
return values
.map(value => `${key}=${value}`)
.join('&')
}
+53
View File
@@ -0,0 +1,53 @@
import { createApiUrl, requestGet, IResponseItems } from './next-lib'
import { IPage, PageCardData } from './types'
import { queryList } from './lib/url'
export type QuerySlice = {
skip: number
limit: number
sortBy: string
fields?: string[]
}
export async function getPagesByTags<T = IPage>(tags: string[], { skip, limit, sortBy, fields }: QuerySlice) {
const tag = queryList('tag', tags)
const field = queryList('field', fields ?? [])
const url = `/api/pages/tags?${tag}&${field}&skip=${skip}&limit=${limit}&sortBy=${sortBy}`
const res = await requestGet<IResponseItems<T>>(createApiUrl(url), null)
return res?.items
}
export async function getPagesCardsByTags(tags: string[]): Promise<PageCardData[]> {
type ResponseItem = {
url: string
title: string
featured: boolean
date: string
coverSrc: string
}
const skip = 0
const limit = 1000
const fields = [
'url',
'title',
'featured',
'date',
'coverSrc'
]
const items = await getPagesByTags<ResponseItem>(tags, { fields, skip, limit, sortBy: '-date' })
if (!Array.isArray(items)) {
return []
}
return items.map(item => ({
url: item.url,
title: item.title,
featured: item.featured,
date: new Date(item.date),
coverSrc: item.coverSrc,
}))
}
-4
View File
@@ -6,10 +6,6 @@
hyphens: auto;
margin-bottom: var(--post-margin);
&-Head {
margin-bottom: 1.25em;
}
h1 {
margin-bottom: var(--size-s);
}
+17 -25
View File
@@ -8,15 +8,26 @@ export interface IBreadcumbsPart {
href: string
}
export type PageCardData = {
url: string
title: string
date: Date
featured?: boolean
coverSrc?: string
}
export interface IPage {
id: string
title: string
url: string
data: string
date: string
preview: ImageDefinition
description: string
tokens: any[]
tags: ITag[]
breadcrumb?: IBreadcumbsPart[]
featured: boolean
}
export interface IDocument {
@@ -58,31 +69,12 @@ export interface ITag {
href: string
}
export interface IArticle {
id: string
date: string
tags: ITag[]
post:string
featured: boolean
url: string
title: string
preview: ImageDefinition
}
export type Person = {
id: string
position: string
name: [string, string, string]
post: string
diploma: string
edu: string
file: string
hash: string
shortName: string
status: string
url: string
picture: ImageArtifact
preview: ImageDefinition
export enum ImageSize {
big = 3000,
large = 1500,
medium = 1000,
small = 500,
thumbnail = 200,
}
export type ImageArtifact = {
-69
View File
@@ -1,69 +0,0 @@
/**
* Return a closure that compares a Number argument with specified Number
* @param withNumber Number to compare with
* @returns {Function} (i) => {}
*/
export function numberEquals(withNumber) {
return i => i === withNumber;
}
/**
* Return a closure that compares a second Number argument with specified Number
* @param withNumber Number to compare with
* @returns {Function} (i) => {}
*/
export function indexEquals(withNumber) {
return (_, i) => i === withNumber;
}
/**
* Return a reduce function
* [['spring', 'Весена'], ['autumn', 'Осень']] -> 'Весна' or 'Осень' depend on byCondition result
* @param byCondition
* @param map
*/
export function select(byCondition, map = i => i) {
return (result, item) => byCondition(item) ? map(item) : map(result)
}
/**
* Select an item in Array by specified selector function
* @param scope
* @param selector
* @returns {*}
*/
export function choise(scope, selector) {
return scope.reduce(selector, null);
}
export function assign(dict, key, value) {
dict[key] = value;
return dict;
}
export function map(v, fn) {
return value => fn(value) ? v : value;
}
/**
*
* f(g, [x, y, z, ...]) => map of {g(x): x, g(y): y, g(z): z}
*
* @param list
* @param selector
* @returns {*}
*/
export function createMap(selector, list) {
return list.reduce((map, i) => map.set(selector(i), i), new Map());
}
/**
*
* f(g) => x ? g(x) : null
*
* @param {function} fn
* @returns {function(*=): null}
*/
export function nullSafe(fn) {
return i => i ? fn(i) : null;
}
-13
View File
@@ -1,13 +0,0 @@
/**
*
* f([x, y, z, ...]) -> f(v) -> {v.x, v.y, v.z}
*
* @param fields
* @returns {function(*): *}
*/
export function select(fields=[]){
return i => fields.reduce((out, key) => {
out[key] = i[key];
return out;
}, {});
}
-8
View File
@@ -1,8 +0,0 @@
export function query(list) {
return list
.reduce(
(query, param) => query.concat([`${param[0]}=${param[1]}`]),
[]
)
.join('&');
}
-35
View File
@@ -1,35 +0,0 @@
export function pop(data){
var Populator = function(data){
this.target = data;
}
Populator.prototype = {
populate: function(p){
var self = this;
if(typeof p == "function"){
self.target = p(self.target);
}else if(p instanceof Array){
p.forEach(function(i){
self.target = self.populate(i).result();
});
}
return self;
},
result: function(){
return this.target;
}
};
var fun = new Populator(data);
return fun;
}
export function populate(fn, key){
return (dict) => {
let id = dict[key];
let value = fn(id);
dict[key] = value ? value : id;
return dict;
}
}
-26
View File
@@ -1,26 +0,0 @@
export function sortByPattern(list, pattern, map) {
let weight = i => {
const index = pattern.indexOf(map(i))
return index > -1 ? pattern.length - index : 0
}
return sortByWeight(list, weight)
}
export function sortByWeight(list, fn) {
return list
.map(i => [fn(i), i])
.sort((a, b) => b[0] - a[0])
.map(i => i[1])
}
export function sortBy(fn) {
return (i1, i2) => {
let t1 = fn(i1)
let t2 = fn(i2)
if (t1 < t2) return 1
if (t1 > t2) return -1
return 0
}
}
-22
View File
@@ -1,22 +0,0 @@
import {map} from './common';
/**
* Return a function that accept one String parameter and returns result of replacing.
*
* @param searchValue
* @param replaceValue
*/
export const stringReplace = (searchValue, replaceValue) =>
str => str.replace(searchValue, replaceValue);
let toTrue = map(true, i => i === 'true');
let toFalse = map(false, i => i === 'false');
const boolProcessors = [toTrue, toFalse];
export function stringToBoolean(string){
return boolProcessors
.reduce(
(value, fn) => fn(value),
string
);
}
-1
View File
@@ -1 +0,0 @@
--recursive
-51
View File
@@ -1,51 +0,0 @@
import should from 'should';
import {select} from '../../src/utils/common';
describe('utils', () => {
describe('common.js', () => {
describe('selectItem', () => {
it('should return a function', () => {
let fn = select();
fn.should.be.type('function');
});
it('should accept a function as a first parameter and use it as byCondition', () => {
let sample = [[1235, 'hello'], [2352, 'banana']];
let fn = select(
i => i[0] === 2352 && i[1] === 'banana' && i.length === 2,
i => i[1]
);
let result = sample.reduce(fn);
result.should.be.equal('banana');
});
it('should accept a function as a second parameter and use it as mapping function', () => {
let sample = [[1235, 'hello'], [2352, 'banana']];
let fn = select(
i => i[0] === 2352,
i => i[1]
);
let result = sample.reduce(fn);
result.should.be.equal('banana');
});
it('should pass with one parameter', () => {
let sample = ['hello', 'lol', 'banana'];
let result = sample.reduce(select(i => i === 'banana'));
result.should.be.equal('banana');
});
it('should get a null in reduce result if reduce calls with second parameter that is null', () => {
let sample = ['hello', 'lol', 'banana'];
let result = sample.reduce(select(i => i === 'apple'), null);
should.not.exist(result);
});
});
});
});
-21
View File
@@ -1,21 +0,0 @@
import should from 'should';
import {query} from '../../src/utils/net';
describe('utils', () => {
describe('net.js', () => {
describe('query', () => {
let sample = [['count', 10], ['portion', 5], ['hello', 'lol']];
it('should combine query params', () => {
let params = query(sample);
params.should.be.equal('count=10&portion=5&hello=lol');
});
it('should return string', () => {
let params = query(sample);
params.should.be.type('string');
});
});
});
});