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`
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* Date -> Unix time
|
||||
*
|
||||
* @param date
|
||||
*/
|
||||
export const timestamp = (date: Date) => date.getTime()
|
||||
@@ -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]}`
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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 +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,
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}))
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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;
|
||||
}, {});
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export function query(list) {
|
||||
return list
|
||||
.reduce(
|
||||
(query, param) => query.concat([`${param[0]}=${param[1]}`]),
|
||||
[]
|
||||
)
|
||||
.join('&');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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}'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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__()
|
||||
|
||||
@@ -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: ''.format(img=i),
|
||||
images
|
||||
))
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Generated
+5
-5
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
.image {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image.pending {
|
||||
/* opacity: 0; */
|
||||
}
|
||||
|
||||
.image.loaded {
|
||||
/* opacity: 1; */
|
||||
}
|
||||
@@ -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.
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.pageMeta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pageMeta.center {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
export function titleCase(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
}))
|
||||
}
|
||||
@@ -6,10 +6,6 @@
|
||||
hyphens: auto;
|
||||
margin-bottom: var(--post-margin);
|
||||
|
||||
&-Head {
|
||||
margin-bottom: 1.25em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: var(--size-s);
|
||||
}
|
||||
|
||||
+17
-25
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}, {});
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export function query(list) {
|
||||
return list
|
||||
.reduce(
|
||||
(query, param) => query.concat([`${param[0]}=${param[1]}`]),
|
||||
[]
|
||||
)
|
||||
.join('&');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
--recursive
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user