Merge pull request #3 from tmshv/dev

Nextjs version
This commit is contained in:
tmshv authored and GitHub committed 2019-10-13 21:19:11 +03:00
commit 3f90e35336
220 files changed
+11864 -15736

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
],
"plugins": [
"transform-react-jsx",
"transform-object-rest-spread",
"@babel/plugin-proposal-object-rest-spread",
"transform-class-properties"
]
}
+3 -1
View File
@@ -8,12 +8,14 @@ private*.json
/node_modules
/bower_components
/modules/hudozka-api-v1/lib
/public
/build
/out
dist
.next
**/env
**/venv
**/__pycache__
**/*.pyc
/coverage
**/.vscode
*.log
+42 -21
View File
@@ -1,27 +1,48 @@
# Build
FROM node:10 as build
WORKDIR /app
# # Build
# FROM node:10 as build
# WORKDIR /app
COPY package.json package-lock.json ./
RUN npm i
COPY webpack.config.js gulpfile.js .babelrc ./
COPY robots.txt ./
COPY src ./src
RUN npm run gulp
RUN npm run build-app
RUN npm run out
# COPY package.json package-lock.json ./
# RUN npm i
# COPY webpack.config.js gulpfile.js .babelrc ./
# COPY robots.txt ./
# COPY src ./src
# RUN npm run gulp
# RUN npm run build-app
# RUN npm run out
# # Run
# FROM node:8-alpine
# WORKDIR /app
# COPY package.json package-lock.json ./
# RUN npm i --production
# COPY --from=build /app/out ./src
# COPY --from=build /app/public ./public
# EXPOSE 3000
# CMD npm start
# FROM node:12-alpine as build
# WORKDIR /app
# COPY package.json package-lock.json ./
# RUN npm ci
# COPY . .
# RUN npm run build
# RUN npm run start & npm run export
# Run
FROM node:8-alpine
WORKDIR /app
FROM nginx:stable-alpine
COPY package.json package-lock.json ./
RUN npm i --production
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/out ./src
COPY --from=build /app/public ./public
EXPOSE 3000
CMD npm start
WORKDIR /usr/share/nginx/html
# COPY --from=build /app/dist /usr/share/nginx/html
COPY ./dist /usr/share/nginx/html
-8
View File
@@ -1,8 +0,0 @@
{
"uid": "hudozka",
"append": true,
"watch": false,
"script": "index.js",
"sourceDir": "/home/hoster/www/art.shburg.org/out",
"wordingDir": "/home/hoster/www/art.shburg.org"
}
-58
View File
@@ -1,58 +0,0 @@
const gulp = require('gulp')
const postcss = require('gulp-postcss')
const sass = require('gulp-sass')
const imageMin = require('gulp-imagemin')
const concat = require('gulp-concat')
const autoprefixer = require('autoprefixer')
const cssnano = require('cssnano')
gulp.task('style', () => {
const processors = [
autoprefixer({
browsers: ['last 1 version']
}),
cssnano({
zindex: false
})
]
return gulp.src('./src/style/style.scss')
.pipe(sass())
.pipe(postcss(processors))
.pipe(concat('style.css'))
.pipe(gulp.dest('./public'))
})
gulp.task('copy fonts', () => {
return gulp.src('./src/assets/fonts/*/*')
.pipe(gulp.dest('./public/fonts'))
})
gulp.task('copy graphics', () => {
return gulp.src('./src/assets/graphics/*')
.pipe(gulp.dest('./public/graphics'))
})
gulp.task('copy 3rdparty', () => {
return gulp.src('./src/3rdparty/*')
.pipe(gulp.dest('./public'))
})
gulp.task('copy robots.txt', () => {
return gulp.src('./robots.txt')
.pipe(gulp.dest('./public'))
})
gulp.task('copy views', () => {
return gulp.src('./src/views/**')
.pipe(gulp.dest('./out/views'))
})
gulp.task('imagemin', () => {
return gulp.src('./src/assets/graphics/**/*')
.pipe(imageMin())
.pipe(gulp.dest('./public/graphics'))
})
gulp.task('copy', gulp.series('copy views', 'copy fonts', 'copy graphics', 'copy robots.txt', 'copy 3rdparty'))
gulp.task('default', gulp.series('style', 'copy', 'imagemin'))
-16
View File
@@ -1,16 +0,0 @@
#!/bin/sh
dir_path="$1"
build_file="$2"
app="hudozka"
script="forever.json"
cd ${dir_path}
gunzip -c ${build_file} | tar xopf -
#rm build.tar
npm install --production --loglevel error
echo "Deploying finish. Please restart hudozka.service manually"
-27
View File
@@ -1,27 +0,0 @@
FROM alpine:3.5
RUN apk add --no-cache python3 && \
apk add --no-cache py3-lxml && \
python3 -m ensurepip && \
rm -r /usr/lib/python*/ensurepip && \
pip3 install --upgrade pip setuptools && \
rm -r /root/.cache
LABEL maintainer "Roman Timashev <roman@tmshv.ru>"
ENV INSTALL_PATH /app
RUN mkdir -p $INSTALL_PATH
WORKDIR /$INSTALL_PATH
COPY requirements.txt .
RUN pip3 install -r requirements.txt
COPY app ./app
ENV DB_URI "mongodb://localhost:27017"
ENV DB_USER ""
ENV DB_PASSWORD ""
ENV DB_NAME "hudozka"
EXPOSE 5000
CMD python3 app/main.py
-10
View File
@@ -1,10 +0,0 @@
# Feed
Link to feed: [art.shlisselburg.org/feed](https://art.shlisselburg.org/feed)
## How to deploy with Docker
```bash
docker build -t hudozka/feed .
docker run -d --name hudozka-feed -p 18050:5000 -e DB_USER="<DB_USER>" -e DB_PASSWORD="<DB_PASSWORD>" -e DB_URI="<DB_URI>" hudozka/feed
```
-13
View File
@@ -1,13 +0,0 @@
from pymongo import MongoClient
from settings import db_uri, db_name, db_user, db_password
db = MongoClient(db_uri)[db_name]
if db_user and db_password:
auth_status = db.authenticate(db_user, db_password)
if not auth_status:
print('Failed to authenticate user')
exit(1)
def collection(name):
return db[name]
-39
View File
@@ -1,39 +0,0 @@
import html
from feedgen.feed import FeedGenerator
from db import collection
def get_items(collection_name, query={}):
return list(
collection(collection_name).find(query)
)
def get_feed():
events = get_items('events')
news = get_items('timeline', {'type': 'post'})
articles = sorted(events + news, key=lambda item: item['date'], reverse=True)
fg = FeedGenerator()
fg.id('art.shlisselburg.org')
fg.title('Шлиссельбургская ДХШ')
fg.link(href='https://art.shlisselburg.org', rel='alternate')
fg.logo('https://static.shlisselburg.org/art/graphics/favicon-144.png')
fg.link(href='https://art.shlisselburg.org/feed', rel='self')
fg.language('ru')
for article in articles:
content = html.unescape(article['post'])
article_id = article['id']
url = 'https://art.shlisselburg.org/article/{id}'.format(id=article_id)
fe = fg.add_entry()
fe.id(article_id)
fe.title(article['title'])
fe.content(content, None, 'CDATA')
fe.link({'href': url})
return fg.atom_str(pretty=True)
-5
View File
@@ -1,5 +0,0 @@
from server import app
from settings import host
if __name__ == '__main__':
app.run(host=host)
-14
View File
@@ -1,14 +0,0 @@
from flask import Flask
from feed import get_feed
app = Flask(__name__)
@app.route('/')
def root():
feed = get_feed()
if not feed:
return '', 404
return feed
-7
View File
@@ -1,7 +0,0 @@
import os
db_uri = os.getenv('DB_URI', 'mongodb://localhost:27017')
db_name = os.getenv('DB_NAME', 'hudozka')
db_user = os.getenv('DB_USER', None)
db_password = os.getenv('DB_PASSWORD', None)
host = os.getenv('APP_HOST', '0.0.0.0')
-5
View File
@@ -1,5 +0,0 @@
pymongo==3.4.0
Flask==0.12
redis==2.10.5
PyYAML==3.12
feedgen==0.5.1
+628
View File
@@ -0,0 +1,628 @@
const withCss = require('@zeit/next-css')
const withSass = require('@zeit/next-sass')
const { range } = require('lodash')
function createPath(page, map, query, items) {
return items.reduce((acc, path) => {
return {
...acc,
[path]: {
page,
query: query(map(path)),
}
}
}, {})
}
module.exports = withCss(withSass({
exportTrailingSlash: true,
serverRuntimeConfig: {
dbUri: '',
// Will only be available on the server side
// mySecret: 'secret',
// secondSecret: process.env.SECOND_SECRET // Pass through env variables
},
exportPathMap: async (defaultPathMap, { dev }) => {
if (dev) {
return null
}
const articlesPageSize = 15
const articlesLastPage = 8
return {
// ...defaultPathMap,
'/': { page: '/', query: { pageSize: articlesPageSize } },
'/documents': { page: '/documents' },
'/gallery': { page: '/gallery' },
'/collective': { page: '/collective' },
'/schedule': { page: '/schedule' },
...createPath(
'/articles/[page]',
x => x.replace('/articles/', ''),
page => ({ page, pageSize: articlesPageSize }),
range(1, articlesLastPage + 1).map(x => `/articles/${x}`),
),
...createPath(
'/next',
x => x,
() => ({}),
[
'/info/vacancy',
'/info/new',
'/info/support',
'/info/education',
'/info/standards',
'/info/main',
'/info/paid-services',
'/info',
'/info/tools',
'/info/grants',
'/info/structure',
'/info/finances',
'/courses',
'/courses/batik',
'/courses/ceramics',
'/courses/composition',
'/courses/costume-composition',
'/courses/cg',
'/courses/scale-modeling',
'/courses/drawing',
'/courses/easel-composition',
'/courses/felting',
'/info/history',
'/courses/art-history',
'/info/documents',
'/info/online-education',
'/info/education/listochki',
'/info/education/matreshki',
'/info/education/perishki',
'/info/support/objects',
'/info/support/tools',
'/info/support/sport',
'/info/support/network',
'/info/education/painting',
'/info/education/art-for-beginners',
'/join',
'/info/audit',
'/summer-school/2017',
'/info/health-saving',
'/summer-school/2017',
'/info/education/painting',
'/info/education/art-for-beginners',
'/info/education/listochki',
'/info/education/matreshki',
'/info/education/perishki',
'/info/vacancy',
'/info/online-education',
'/info/documents',
'/info/new',
'/info/support',
'/info/support/objects',
'/info/support/tools',
'/info/education',
'/info/standards',
'/info/main',
'/info/paid-services',
'/join',
'/courses',
'/info/support/network',
'/info',
'/info/tools',
'/info/support/sport',
'/info/grants',
'/info/structure',
'/info/audit',
'/info/finances',
'/info/health-saving',
'/info/history',
'/courses/batik',
'/courses/felting',
'/courses/art-history',
'/courses/ceramics',
'/courses/composition',
'/courses/costume-composition',
'/courses/cg',
'/courses/scale-modeling',
'/courses/drawing',
'/courses/easel-composition',
'/courses/listochki',
'/courses/matreshki',
'/courses/perishki',
'/neighbors',
'/neighbors/2017',
'/neighbors/2017/continuing-the-chronicle',
'/neighbors/2017/favorite-view-of-native-place',
'/neighbors/2016',
'/neighbors/2016/continuing-the-chronicle',
'/neighbors/2016/quite-provincial-life',
'/neighbors/2016/illustrations-rimsky-korsakov-works',
'/neighbors/2017/quite-provincial-life',
'/neighbors/2016/favorite-view-of-native-place',
'/neighbors/2015',
'/neighbors/2015/industrial-landscape',
'/neighbors/2015/favorite-view-of-native-place',
'/neighbors/2015/continuing-the-chronicle',
'/neighbors/2015/these-places-remember-the-war',
'/info/corruption',
'/domik',
'/courses/ladushki',
'/info/security',
'/neighbors/2018/continuing-the-chronicle',
'/neighbors/2018',
'/neighbors/2018/favorite-view-of-native-place',
'/neighbors/2018/industrial-landscape',
'/info/navigator',
]
),
...createPath(
'/teacher/[person]',
x => x.replace('/teacher/', ''),
person => ({ person }),
[
'/teacher/as-timasheva',
'/teacher/va-sarzhin',
'/teacher/vv-voronova',
'/teacher/ea-burovtseva',
'/teacher/ey-tarasova',
'/teacher/iv-khramov',
'/teacher/in-vturina',
'/teacher/ma-khramova',
'/teacher/mg-timasheva',
'/teacher/my-valkova',
'/teacher/nv-andreeva',
'/teacher/od-gogoleva',
'/teacher/rk-timashev',
'/teacher/sa-latipova',
],
),
...createPath(
'/article/[article]',
x => x.replace('/article/', ''),
person => ({ person }),
[
'/article/2009-ntv',
'/article/2010-tv100',
'/article/oblastnoi-plener-udalsia-na-slavu',
'/article/pervyi-kinopokaz',
'/article/30',
'/article/kinopokaz',
'/article/elka-dlia-malyshei-2015',
'/article/vystavka-kukly-korennykh-narodov-leningradskoi-oblasti',
'/article/vystavka-kukly-srednei-azii-i-kavkaza',
'/article/detskii-sadik',
'/article/think-try-create',
'/article/konkurs-volshebnaia-kist-na-nekrasova',
'/article/master-klass-po-izgotovleniiu-vatnykh-kukol-na-nekrasova',
'/article/2015-pridumyvai-probui-tvori',
'/article/vystavka-vesna-2015',
'/article/exhibition-70',
'/article/vesna-2015',
'/article/vystavka-letnikh-rabot',
'/article/kinopokaz-chukovsky',
'/article/kino-domik',
'/article/vystavka-v-biblioteke-nekrasova',
'/article/konkurs-volshebnaia-kist',
'/article/russian-history-competition',
'/article/master-klass-pogoleshechnaia-kukla',
'/article/2016-pridumyvai-probui-tvori',
'/article/prazdnik-v-muzykalnoi-shkole-shlisselburga',
'/article/nabor-detei-s-7-mi-do-9-ti-let',
'/article/serebro-za-vizitku',
'/article/talant-vospityvaetsia-v-trude',
'/article/2016-10-kaniculi',
'/article/2016-tikhvin',
'/article/2016-mi-sosedi',
'/article/master-klass-po-kalligrafii',
'/article/2016-master-class-calligraphy',
'/article/2016-11-kaniculi',
'/article/vtoroi-master-klass-po-kalligrafii',
'/article/2016-master-class-vatnoe-papie-mashe',
'/article/2016-ny-master-class',
'/article/2016-master-class-calligraphy-2',
'/article/v-nominatsii-sovremennaia-kukla-pobedili-iolochnye-ukrasheniia-shlisselburgskoi-masteritsy',
'/article/anons-master-klassa-kukly-voennykh-let',
'/article/master-klass-kukly-voennykh-let',
'/article/deiateli-kultury-gordost-leningradskoi-oblasti',
'/article/gostevoi-dom-shlisselburg',
'/article/uchastie-v-konkurse-zvuk-i-forma',
'/article/vizit-gubernatora-leningradskoi-oblasti',
'/article/anons-konkursa-smotrimvidim',
'/article/anons-letnei-shkoly',
'/article/master-class-papier-mache-2017',
'/article/cats-master-class-2017',
'/article/vyborg-announcement-2017',
'/article/vyborg-info-2017',
'/article/smotrim-vidim-final-announcement-2017',
'/article/summer-school-info-2017',
'/article/smotrim-vidim-final-2017',
'/article/vyborg-2017',
'/article/2017-info-new',
'/article/preparing-2017',
'/article/sarzhin-star',
'/article/toropets',
'/article/sarzhin-october',
'/article/2017-10-kaniculi',
'/article/2017-10-workshop',
'/article/shlisselshved',
'/article/shlisselburg-315',
'/article/scholarship-2017',
'/article/sarzhin-70',
'/article/2017-10-workshop-ceramics',
'/article/2017-katavasia',
'/article/2017-golden-hands',
'/article/2016-liditse',
'/article/2017-the-authors-style',
'/article/2017-dpi-methods',
'/article/2017-oreshek-workshop',
'/article/2017-neighbors-vybord-rewarding',
'/article/2017-stars',
'/article/2017-petrovskaya-aquatoria-trip',
'/article/2017-dnt',
'/article/silver-pencil-2018',
'/article/magic-brush-2018',
'/article/blokada-workshop-2018',
'/article/best-teacher-2018',
'/article/save-nature-competition-2018',
'/article/win-day-2018',
'/article/site-iii-2018',
'/article/israel-2018',
'/article/blokada-workshop-2-2018',
'/article/plain-air-2018',
'/article/graduation-2018',
'/article/blokada-workshop-3-2018',
'/article/2012-timestamp',
'/article/plain-air-june-2018',
'/article/berezka-2018',
'/article/plain-air-pro-2018',
'/article/2018-neighbors-kronshtadt',
'/article/vyborg-competition-2018',
'/article/norway-2018',
'/article/family-album-2018',
'/article/2018-neighbors-ceremony',
'/article/2018-100',
'/article/2018-buduschee-moey-strany',
'/article/2019-nv-andreeva',
'/article/2019-blokada-workshop',
'/article/2018-culture-star',
'/article/2018-guests-from-germany',
'/article/2018-grants',
'/article/2018-inclusive-education',
'/article/2018-neighbors-announcement',
'/article/2019-fortuna',
'/article/2019-orchestra',
'/article/2019-magic-brush',
'/article/2019-silver-pencil',
'/article/2019-spring-subbotnik',
'/article/2019-win500',
'/article/2019-korushka',
'/article/2019-pleinair',
],
),
...createPath(
'/album/[album]',
x => x.replace('/album/', ''),
album => ({ album }),
[
'/album/2012-tekhnika-mokrogo-valianiia',
'/album/2013-valianie-zverushki',
'/album/2013-shlisselburg',
'/album/2013-natiurmort',
'/album/2015-kukly-valianie',
'/album/2014-kollazh',
'/album/2014-shlisselburg',
'/album/2014-elochnye-domiki',
'/album/2014-elochnye-igrushki',
'/album/2014-zhivopis-narody-severa',
'/album/2015-maketirovanie-doma-peterburga',
'/album/2015-zhivopis',
'/album/2015-maketirovanie',
'/album/2015-podvodnyi-mir-stankovaia-kompozitsiia',
'/album/2015-risunok',
'/album/2015-skazki',
'/album/2015-stankovaia-kompozitsiia',
'/album/2015-tsvety-stankovaia-kompozitsiia',
'/album/2015-graviura',
'/album/2015-guash',
'/album/2015-shlisselburg',
'/album/2015-dpk-portret-v-tekhnike-applikatsiia',
'/album/2015-nominatsiia-navstrechu-pobede',
'/album/2015-domiki',
'/album/2015-skulptury-na-temu-70-let-pobedy',
'/album/2015-history-competition',
'/album/2015-rozhdestvenskaia-skazka',
'/album/2016-konkurs-zimnie-zabavy',
'/album/2016-konkurs-na-kryliakh-babochek',
'/album/2016-konkurs-narodnye-motivy',
'/album/around-the-world',
'/album/comod',
'/album/leto-domovenok',
'/album/2016-tikhvin-mi-sosedi',
'/album/2017-konkurs-budushchee-moei-strany',
'/album/2017-konkurs-puteshestvie-k-dinozavram',
'/album/2017-konkurs-eto-mamin-den',
'/album/2017-konkurs-forma-i-zvuk',
'/album/pleinair-competition-2017',
'/album/ceramics-2017',
'/album/vyborg-makets-2017',
'/album/ceramics-march-2018',
'/album/ceramics-june-2018',
'/album/batik-june-2018',
'/album/village-pictures-june-2018',
'/album/fortress-june-2018',
'/album/baloons-june-2018',
'/album/drawing-jule-2018',
'/album/drawing-june-2018',
'/album/painting-june-2018',
],
),
...createPath(
'/document/[document]',
x => x.replace('/document/', ''),
document => ({ document }),
[
'/document/documents-2017-2018-kalendarnyi-grafik-2017-2018-pdf',
'/document/documents-arkhiv-obrazovatelnaia-litsenziia-2013-pdf',
'/document/documents-arkhiv-plan-vnutrishkolnogo-kontrolia-2012-pdf',
'/document/documents-arkhiv-polozhenie-o-vnutrishkolnom-kontrole-2012-pdf',
'/document/documents-arkhiv-polozhenie-o-pedagogicheskom-sovete-pdf',
'/document/documents-arkhiv-polozhenie-o-prieme-perevode-otchislenii-pdf',
'/document/documents-arkhiv-polozhenie-o-propusknom-rezhime-2015-pdf',
'/document/documents-arkhiv-pravila-i-polozhenie-o-prieme-v-shkolu-pdf',
'/document/documents-arkhiv-ustav-2013-pdf',
'/document/documents-dokumenty-dlia-postupleniia-dogovor-o-pozhertvovanii-pdf',
'/document/documents-dokumenty-dlia-postupleniia-dogovor-o-sotrudnichestve-pdf',
'/document/documents-dokumenty-dlia-postupleniia-zaiavlenie-roditelei-pdf',
'/document/documents-instruktsii-instruktsiia-po-organizatsii-okhrany-zhizni-i-zdorovia-detei-pdf',
'/document/documents-instruktsii-instruktsiia-po-okhrane-zhizni-i-zdorovia-detei-pdf',
'/document/documents-instruktsii-plan-vnutrishkolnogo-kontrolia-2016-pdf',
'/document/documents-instruktsii-pravila-vnutrennego-trudovogo-rasporiadka-pdf',
'/document/documents-materialno-tekhnicheskoe-obespechenie-i-osnashchennost-obrazovatelnogo-protsessa-vnutrishkolnyi-metodicheskii-bibliotechnyi-fond-pdf',
'/document/documents-materialno-tekhnicheskoe-obespechenie-i-osnashchennost-obrazovatelnogo-protsessa-dogovor-na-meditsinskoe-obsluzhivanie-pdf',
'/document/documents-materialno-tekhnicheskoe-obespechenie-i-osnashchennost-obrazovatelnogo-protsessa-dogovor-na-okazanie-uslug-po-organizatsii-pitaniia-pdf',
'/document/documents-materialno-tekhnicheskoe-obespechenie-i-osnashchennost-obrazovatelnogo-protsessa-dogovor-na-okazanie-uslug-sviazi-pdf',
'/document/documents-materialno-tekhnicheskoe-obespechenie-i-osnashchennost-obrazovatelnogo-protsessa-materialno-tekhnicheskoe-obespecheninie-obrazovatelnogo-protsessa-pdf',
'/document/documents-obrazovanie-dopolnitelnaia-obshcherazvivaiushchaia-programma-khudozhestvennoi-napravlennosti-izobrazitelnoe-iskusstvo-dlia-nachinaiushchikh-pdf',
'/document/documents-obrazovanie-dopolnitelnaia-obshcherazvivaiushchaia-programma-khudozhestvennoi-napravlennosti-dlia-obuchaiushchikhsia-podgotovitelnykh-klassov-v-dkhsh-listochki-pdf',
'/document/documents-obrazovanie-dopolnitelnaia-obshcherazvivaiushchaia-programma-khudozhestvennoi-napravlennosti-dlia-obuchaiushchikhsia-podgotovitelnykh-klassov-v-dkhsh-matreshki-pdf',
'/document/documents-obrazovanie-dopolnitelnaia-obshcherazvivaiushchaia-programma-khudozhestvennoi-napravlennosti-dlia-obuchaiushchikhsia-podgotovitelnykh-klassov-v-dkhsh-peryshki-pdf',
'/document/documents-obrazovanie-dopolnitelnaia-predprofessionalnaia-obshcheobrazovatelnaia-programma-v-oblasti-izobrazitelnogo-iskusstva-zhivopis-pdf',
'/document/documents-obrazovanie-dpop-zhivopis-pdf',
'/document/documents-obrazovanie-kalendarnyi-uchebnyi-grafik-pdf',
'/document/documents-obrazovanie-opisanie-obrazovatelnoi-programmy-izobrazitelnoe-iskusstvo-dlia-nachinaiushchikh-pdf',
'/document/documents-obrazovanie-opisanie-obshcheobrazovatelnoi-programmy-v-oblasti-izobrazitelnogo-iskusstva-zhivopis-pdf',
'/document/documents-obrazovanie-polozhenie-o-komplektovanii-i-napolniaemosti-grupp-pdf',
'/document/documents-obrazovanie-polozhenie-o-poriadke-obucheniia-po-individualnomu-uchebnomu-planu-pdf',
'/document/documents-obrazovanie-polozhenie-o-iazyke-obrazovaniia-v-mbudo-shlisselburgskaia-dkhsh-pdf',
'/document/documents-obrazovanie-programma-obucheniia-2017-pdf',
'/document/documents-obrazovanie-uchebnyi-plan-po-izobrazitelnomu-iskusstvu-pdf',
'/document/documents-obrazovanie-uchebnyi-plan-po-dopolnitelnoi-obshcherazvivaiushchei-podgotovitelnoi-programme-po-izobrazitelnomu-iskusstvu-pdf',
'/document/documents-obrazovanie-uchebnyi-plan-po-dopolnitelnoi-predprofessionalnoi-obshcheobrazovatelnoi-programme-v-oblasti-izobrazitelnogo-iskusstva-zhivopis-pdf',
'/document/documents-osnovnye-dokumenty-egriul-pdf',
'/document/documents-osnovnye-dokumenty-inn-pdf',
'/document/documents-osnovnye-dokumenty-kopiia-resheniia-o-naznachenii-rukovoditelia-pdf',
'/document/documents-osnovnye-dokumenty-obrazovatelnaia-litsenziia-2015-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-o-vnutrishkolnom-kontrole-2017-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-o-dobrovolnom-pozhertvovanii-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-o-kodekse-professionalnoi-etiki-pedagogicheskikh-rabotnikov-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-o-pedagogicheskom-sovete-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-o-poriadke-vydachi-dokumenta-ob-obuchenii-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-o-pravilakh-vnutrennego-rasporiadka-obuchaiushchikhsia-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-o-prieme-v-shkolu-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-o-propusknom-rezhime-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-o-sovete-shkoly-2017-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-o-formakh-periodichnosti-i-poriadke-tekushchego-kontrolia-uspevaemosti-promezhutochnoi-attestatsii-obuchaiaiushchikhsia-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-ob-appeliatsionnoi-komissii-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-ob-itogovoi-attestatsii-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-ob-obshchem-sobranii-rabotnikov-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-ob-usloviiakh-provedeniia-individualnogo-otbora-dlia-postupaiushchikh-s-ogranichennymi-vozmozhnostiami-zdorovia-pdf',
'/document/documents-osnovnye-dokumenty-polozhenie-po-promezhutochnoi-attestatsii-pdf',
'/document/documents-osnovnye-dokumenty-postanovlenie-o-sozdanii-munitsipalnogo-obrazovatelnogo-uchrezhdeniia-kultury-detskaia-khudozhestvennaia-shkola-pdf',
'/document/documents-osnovnye-dokumenty-programma-razvitiia-na-2017-2020-gg-pdf',
'/document/documents-osnovnye-dokumenty-rezhim-zaniatii-obuchaiushchikhsia-pdf',
'/document/documents-osnovnye-dokumenty-reshenie-o-sozdanii-shkoly-pdf',
'/document/documents-osnovnye-dokumenty-svidetelstvo-na-pomeshchenie-pdf',
'/document/documents-osnovnye-dokumenty-ustav-2015-pdf',
'/document/documents-plany-fkhd-munitsipalnoe-zadanie-na-2017-god-i-na-planovyi-period-2018-i-2019-godov-pdf',
'/document/documents-plany-fkhd-otchet-o-vypolnenii-munitsipalnogo-zadaniia-2016-god-pdf',
'/document/documents-plany-fkhd-plan-finansovoi-khoziaistvennoi-deiatelnosti-na-2016-god-pdf',
'/document/documents-plany-fkhd-plan-finansovoi-khoziaistvennoi-deiatelnosti-na-2017-god-pdf',
'/document/documents-platnye-obrazovatelnye-uslugi-dogovor-ob-okazanii-platnykh-obrazovatelnykh-uslug-pdf',
'/document/documents-platnye-obrazovatelnye-uslugi-zaiavlenie-na-platnye-obrazovatelnye-uslugi-pdf',
'/document/documents-platnye-obrazovatelnye-uslugi-polozhenie-o-poriadke-predostavleniia-platnykh-obrazovatelnykh-uslug-pdf',
'/document/documents-platnye-obrazovatelnye-uslugi-prikaz-ob-ustanovlenii-razmera-platnykh-obrazovatelnykh-uslug-pdf',
'/document/documents-prochee-2016-zaiavka-na-uchastie-v-44-mvkhpd-v-liditse-pdf',
'/document/documents-prochee-2016-rezultat-samoobsledovaniia-pdf',
'/document/documents-prochee-dostizheniia-obuchaiushchikhsia-v-2015-2016-godu-pdf',
'/document/documents-prochee-litsenziia-spbimi-pdf',
'/document/documents-prochee-otsenka-kachestva-uslug-uchrezhdeniia-kultury-pdf',
'/document/documents-prochee-polozhenie-smotrim-vidim-pdf',
'/document/documents-prochee-rasporiazhenie-o-prisvoenii-stipendii-2017-2018-pdf',
'/document/documents-prochee-rasporiazhenie-ot-30-05-2017-pdf',
'/document/documents-prochee-spisok-materialov-dlia-zaniatiia-v-khudozhestvennoi-shkole-pdf',
'/document/documents-samoobsledovanie-deiatelnosti-otchet-o-rezultatakh-samoobsledovaniia-01-04-2016-pdf',
'/document/documents-samoobsledovanie-deiatelnosti-otchet-o-rezultatakh-samoobsledovaniia-01-04-2017-pdf',
'/document/documents-tekushchii-kontrol-akt-proverki-komiteta-obshchego-i-professionalnogo-obrazovaniia-leningradskoi-oblasti-2017-pdf',
'/document/documents-uchebnye-programmy-izobrazitelnoe-iskusstvo-dlia-nachinaiushchikh-pdf',
'/document/documents-uchebnye-programmy-programma-po-predmetu-skulptura-pdf',
'/document/documents-uchebnye-programmy-progrmma-po-predmetu-plener-pdf',
'/document/documents-uchebnye-programmy-uchebnaia-programma-zhivopis-pdf',
'/document/documents-uchebnye-programmy-uchebnaia-programma-istoriia-iskusstva-pdf',
'/document/documents-uchebnye-programmy-uchebnaia-programma-prikladnaia-kompozitsiia-pdf',
'/document/documents-uchebnye-programmy-uchebnaia-programma-risunok-pdf',
'/document/documents-uchebnye-programmy-uchebnaia-programma-stankovaia-kompozitsiia-pdf',
'/document/documents-finansovo-khoziaistvennaia-deiatelnost-balans-gosudarstvennogo-(munitsipalnogo)-uchrezhdeniia-pdf',
'/document/documents-finansovo-khoziaistvennaia-deiatelnost-otchet-o-finansovykh-rezultatakh-deiatelnosti-uchrezhdeniia-pdf',
'/document/documents-finansovo-khoziaistvennaia-deiatelnost-otchet-ob-ispolnenii-uchrezhdeniem-plana-ego-finansovo-khoziaistvennoi-deiatelnosti-forma-737-kfo-4-pdf',
'/document/documents-finansovo-khoziaistvennaia-deiatelnost-otchet-ob-ispolnenii-uchrezhdeniem-plana-ego-finansovo-khoziaistvennoi-deiatelnosti-forma-737-kfo-5-pdf',
'/document/documents-finansovo-khoziaistvennaia-deiatelnost-otchet-ob-ispolnenii-uchrezhdeniem-plana-ego-finansovo-khoziaistvennoi-deiatelnosti-pdf',
'/document/pages-finansovo-khoziaistvennaia-deiatelnost-balans-pdf',
'/document/370f8cc297f40d170470e8768ddd227c',
'/document/792bf2823b7840318b02f7d2758af788',
'/document/f574413240ff8ff35091f023976688ee',
'/document/758260df3b7fa615e2fc8a3d2ed9e34c',
'/document/00c90a531da459ab9f10a3d52bac233f',
'/document/2eae32c837bbbd84f403dd7628354b1d',
'/document/fa42795ab1879d009961b5bf69e8c597',
'/document/8f74abfd61354daf8d73e4000b525fdd',
'/document/fe14659e16c98005f4c5168ee3cccc9a',
'/document/98564c3f7c9ce1e5c5f1aa1fe302eaf6',
'/document/6d7359437624b3dd1484d82be66f1ea0',
'/document/4e4ed5431035f97da26eb6b17d88658e',
'/document/dfef1cc07b4317d2d380efc4259dc363',
'/document/8c19548aa39c40a8a2abc4bca20714ca',
'/document/caef7508b14605982d483a5f05756714',
'/document/9f6683f63172a794fd3c6031f781d348',
'/document/acaa47304de042433e26e5eabbbfdeec',
'/document/e094b1f74e8d381b77b510d9f040a616',
'/document/b19092ace94edcf2e7ee0274207b8aa9',
'/document/47883f1e226c48c42e1cb7a9e811bec8',
'/document/ee54f828803e91c7f385b621143f0daf',
'/document/9149c0d182401643c6307ac9ffb1dc65',
'/document/7f229ba5f7882a8a2684289204a8308e',
'/document/37277ec7ddc90a316effef4a3a6ec1c3',
'/document/d50a7953d5a65f4155f7035740594a47',
'/document/ae3d2bc577bfd45f1cbc420f8691de2e',
'/document/b54ad2943689658cccd20c215724bc7d',
'/document/918b27593c43576dfd85b817005f9925',
'/document/77144e7b8eb675d0189c63be1818d964',
'/document/20f2fafe77196edb54857fe0b98438a6',
'/document/e0cdee9ba8a56919f92af7f0363e713f',
'/document/e3e2da7bdd4cdb4fb05a7d72a1c5aa24',
'/document/5883367e713100cfdfa074fbab675907',
'/document/0415c9a50eb0b56651bbcf3d7388a373',
'/document/aa69b76d675e1e30661365deb2e5deef',
'/document/4e35368e42c1840e89c3a3714d3f323b',
'/document/5bd563e2b1b8e0cb20ee2438a5d4888a',
'/document/b19069cc9b3c5be5a92487f9d3385064',
'/document/53f8111c3882521453d889aa84dedea3',
'/document/58e4a7445b19ae0dbe1ee6313cbcee50',
'/document/76a8fc2870f1254a52e80712115ea448',
'/document/dab46182c1c119210b3a8ba4a3079d15',
'/document/52101c7e4bb3bbdc315f93dff8b826ca',
'/document/5a677e08537853a1f4d11a7ae492f8b6',
'/document/2dc1c2e7cfa996c13fb90045c5e4b5fb',
'/document/952e1ed8ec2879eb4d2e300c1b50d890',
'/document/0d20b54c2023563ec0cdff648ed8070d',
'/document/82e9e339678a9385ab44897837a0e7f6',
'/document/ae64123551407d5c325f75d83f58a338',
'/document/bd1ace5ed47ee0958254f6a9732482cc',
'/document/534566e01344b0d1dfec8e3cb0dfc8a9',
'/document/739bd6b500c6bebe742df3d70a0c1fdc',
'/document/00a6c04a383bea80a619d4a86bb1b46f',
'/document/5c9708d3d3c9d6141ed7865a8a3d9566',
'/document/6fec7aa79b5edef99dc5498d8ddc9b28',
'/document/2f4ab4732107b2ce84e2dd9327d2788f',
'/document/d91de578e0a3b5d0878857bebf859124',
'/document/a056376a142ac924485ff6f222ab116d',
'/document/ded3933486acbc63ee0897d768a0af5a',
'/document/f3a21a6b0dc669034542c575dc4a9cb0',
'/document/f08cd96300031e83a55699f257046eb3',
'/document/77b23264aa63ebc2f60811fe46595140',
'/document/58269cb439dd9860b277a772a5651597',
'/document/e87b1ce25b43a4f4613bf35aa1ce17ef',
'/document/2a17275330cfc5933447fa5631a1a137',
'/document/e0726255b3b1275c468432e6825acc8c',
'/document/0d8ba20dc95b0b8cecde26f83821ac60',
'/document/cc02b4688caa24f55da4a36d54d89712',
'/document/3df73e48c1bc1acf4725e158a7abbea2',
'/document/c27d39f66acd8c1e50c66ddc5d0a2d39',
'/document/efd17fc5298d12a4b4a73238e01bc4c1',
'/document/19e094faea42621de8ea362493c6149a',
'/document/869d3c46dc48c31870ff169e1d53ba0f',
'/document/be8201b8f5b0d995faa851c8dee9bb30',
'/document/9e79ef49c9aa132f1b6be66ade0e55ff',
'/document/198f53b9de3c8123b0977d9350c26ace',
'/document/f16c8bc8830d6d5c939fcb3dc3e9dfc8',
'/document/60933dd039b87c2ce5b3f637ec791f71',
'/document/1c05245b4e5889bc0f3cc6e7ee3a022b',
'/document/fb589fa77a5e78a11e5aa90f27fdb10a',
'/document/631962a3257d1196f281f568d780d98f',
'/document/e8877efd3f9234997677b0595b0aaa22',
'/document/4fe9c744e0b1b9de8dec11db5d6ab29f',
'/document/85280b9f3f951c06284ab7fe77a595b6',
'/document/76607ea30bd2a7d86710781b6b9fddd4',
'/document/ec40bc1ed9ce38d563c2bece509b5021',
'/document/074b2b248efc55ae7fe9d157891c59e9',
'/document/1d038d40939be8999fe585f71579f09d',
'/document/8f1764396b0a6c1a0a32235ad4cd69eb',
'/document/4d6524f6c56d7cbeaee699c8b5e37c1b',
'/document/c2ea674b1a3069369b1829e568891b05',
'/document/6c1fb8a2428b7b95ca103363f1273c57',
'/document/e7dae8347ac64981377932e4a93f1237',
'/document/10098b1edb8a242253cdc0636106b880',
'/document/deba55994dd9dc1bd304c360656e3843',
'/document/976dec5025c210b879192929a40b6b27',
'/document/1d48ee849ddea9406a423e4b1e1a7405',
'/document/cc845874fd3b183b0cd17e7138075028',
'/document/81c9a9d965a12ceaf3564128bffd37ad',
'/document/e1720a6b5792a81788f10865ef6ecc79',
'/document/0f0b7ef81ba0d0a2af96a67322110751',
'/document/ba581ef075ea68be80f750ac467e5baf',
'/document/3c9062a9dd236b9d533160c0058c8648',
'/document/27958a0d3dd1e22e7b2f7c68ca753fb3',
'/document/3663f97b6151720d4749574dc9d1e8a4',
'/document/2d21b714a1ba64c24289e2e22d080202',
'/document/7ebc6f2cae58d6505143a20da047dd80',
'/document/1d67ff95477334666d8ae6451f1a32e2',
'/document/c2524a36ba4f9885dcd2abcb1fc00afd',
'/document/c49ef7b1944a96a8b2552aa7fdb60d1b',
'/document/d2b40e5804363a0f326f4d14ad9e0b73',
'/document/3149e573fa3a5b680662a4db84078044',
'/document/c7ccacb1e86467f1250084280e16a58f',
'/document/9b996ba28a688d2b2834b202e14ff317',
'/document/8ddbabaf875dfc1652ed327d9760b6cf',
'/document/4da000c1bbca5231c9768aaeccdffe1a',
'/document/c44153837b872ba80f5d94e7d3641114',
'/document/d99da3c6e7962195f6ae5ce31759d076',
'/document/6025a889bd12cec858d218f68686b916',
'/document/2b2672d859944f1c1b3a63b26ad693ab',
'/document/1a3234642a9bf0493c75511eb3bf98c5',
'/document/554c91d0c84c693f00b8666534f198d1',
'/document/a9bf213af314b8258d70e50ab0a5a3f9',
'/document/7bc0d8238189fd0497d6faf51c1ec6ce',
'/document/27b858d66f1dd403504139389f1c985f',
'/document/2f9873564859bf38c24c2735d3dfc537',
'/document/40256172e2be27799361b3980c39737b',
'/document/c08980a17eaffa33816aa0b9f52d35da',
'/document/26f7783a944186eed230ac234b999da6',
'/document/8b1abc982beae70ac2885856aeaef116',
'/document/d063d09eb6f2d1d765ab904cb211ff61',
'/document/36988b164efadc1348c81916c30076b2',
'/document/765798adfe9b6c75f72c20c716705b82',
'/document/9a0d71c2d8aa33273b9c631af2b47eb0',
'/document/1b8dc4f624b2db9c37097df6011948ca',
'/document/8d4ccb007257e11d7ca7c957d2fa715c',
'/document/7c1fcbed5fa818d1256a33c628eb6695',
'/document/94d1cb5860ec81e00e9fdf24af847a6b',
'/document/808a5a90dda72cc3411ca9c1942f0889',
'/document/b5ed5defd7154073dd44ed6f8cc4c51d',
'/document/65e8b74faa3d2839f08255f8e1835d8e',
'/document/f3af7921743fc47718ca39716fdc32d5',
'/document/3311e9b7d4e57893b94807c3acbde279',
'/document/4ec0139e3466527f71a102e26697b344',
'/document/491c8b0ea8032e4fe40e735711258277',
'/document/50d171c70356995b921c52568f1cf64e',
'/document/033e121e5bc2b487353f512c0258ed1f',
'/document/18831d6d910b64c1a7aaa409d694607c',
'/document/bee4ca5c3a191bd7c43bb7098259bfcb',
'/document/67e0ecd5b41e3b8b18b6d8e809b34f2d',
'/document/ffdd72718d2f9d6f824e0304fb4ed0e8',
'/document/49a03e53293d4e5be6e16b20fe78f107',
'/document/5282789e6ee9ec37b2322abc4011aa80',
'/document/e07f6b95ece80ed82977b096156f4ba8',
'/document/7fdf9aa60d503f2f01f89a8b19b0c770',
'/document/0053224c128c26593816041cfd923fcf',
'/document/c150b448136a61c9f30bbc5372a246d1',
'/document/40f2efd755b816586ea6fc4459159edf',
'/document/d8afbafcf061f1f28cea58f2c2271c68',
'/document/966fda6686c99a28bb1792730ea2057f',
'/document/7c4f6647a6e5e4b5175eb5ecae971320',
'/document/38b8b5be9a2ed9def3a4c80f98a80482',
'/document/e33c22584756e7bb9ee0cd84f7899f4b',
]
),
}
}
}))
+12 -44
View File
@@ -1,48 +1,16 @@
server {
listen 443 ssl;
charset utf-8;
server_name art.shlisselburg.org;
ssl_certificate /etc/letsencrypt/live/art.shlisselburg.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/art.shlisselburg.org/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers AES256+EECDH:AES256+EDH:!aNULL;
location / {
proxy_pass http://hudozka;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
error_log /var/log/nginx/art.shburg.error.log;
access_log /var/log/nginx/art.shburg.access.log;
}
server {
listen 80;
server_name art.shlisselburg.org art.shburg.org;
location ^~ /.well-known/acme-challenge {
root /home/hoster/www/art.shburg.org/public;
}
listen 80;
server_name localhost;
charset utf-8;
location / {
return 301 https://art.shlisselburg.org$request_uri;
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri.html $uri/index.html =404;
}
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream hudozka {
server localhost:1800;
location = /50x.html {
root /usr/share/nginx/html;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
}
+19
View File
@@ -0,0 +1,19 @@
{
"version": 2,
"name": "hudozka",
"alias": [
"hudozka.now.sh"
],
"builds": [
{
"src": "dist/**",
"use": "@now/static"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "dist/$1"
}
]
}
+8795 -9170
View File
File diff suppressed because it is too large. Load diff
+77 -108
View File
@@ -1,109 +1,78 @@
{
"name": "Hudozka",
"private": true,
"version": "4.0.0",
"description": "",
"main": "src/bin/www.js",
"scripts": {
"start": "node src/index.js",
"build-edit": "webpack -w --env.NODE_ENV=production --env.TARGET=edit",
"build-app": "webpack --env.NODE_ENV=production --env.TARGET=app",
"out": "babel src/ -d out/",
"gulp": "gulp",
"test": "mocha --compilers js:babel-register",
"coverage": "istanbul cover mocha -- --compilers js:babel-register"
},
"repository": {
"type": "git",
"url": "https://github.com/tmshv/hudozka"
},
"author": "Roman Timashev",
"license": "ISC",
"dependencies": {
"@mikeljames/draft-js-drag-n-drop-upload-plugin": "^2.0.1",
"async-file": "^2.0.2",
"classnames": "^2.2.5",
"co": "^4.4.0",
"draft-js": "^0.10.5",
"draft-js-alignment-plugin": "^2.0.3",
"draft-js-drag-n-drop-plugin": "^2.0.3",
"draft-js-focus-plugin": "^2.0.4",
"draft-js-image-plugin": "^2.0.5",
"draft-js-plugins-editor": "^2.0.8",
"draft-js-resizeable-plugin": "^2.0.6",
"ejson": "^2.1.2",
"handlebars": "^4.0.5",
"immutable": "^3.8.2",
"instagram-node": "^0.5.1",
"koa": "^2.5.1",
"koa-bodyparser": "^3.0.0",
"koa-compose": "^3.1.0",
"koa-conditional-get": "^1.0.2",
"koa-convert": "^1.2.0",
"koa-cookie": "^1.0.0",
"koa-etag": "^2.0.0",
"koa-helmet": "^1.0.0",
"koa-logger": "^2.0.0",
"koa-mount": "^2.0.0",
"koa-route": "^3.1.0",
"koa-session": "^4.0.0",
"koa-static": "^3.0.0",
"markdown": "^0.5.0",
"moment": "^2.22.2",
"mongodb": "^2.1.14",
"mz": "^2.4.0",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-redux": "^4.4.8",
"react-router": "^4.2.0",
"react-router-redux": "^4.0.8",
"redux": "^3.7.2",
"redux-thunk": "^2.2.0",
"request-promise": "^2.0.1",
"sitemap": "^1.5.0",
"thunkify": "^2.1.2"
},
"devDependencies": {
"@babel/cli": "^7.0.0-beta.47",
"@babel/core": "^7.0.0-beta.47",
"@babel/plugin-proposal-class-properties": "^7.0.0-beta.47",
"@babel/polyfill": "^7.0.0-beta.47",
"@babel/preset-env": "^7.0.0-beta.47",
"@babel/register": "^7.0.0-beta.47",
"autoprefixer": "^6.3.6",
"babel-loader": "^8.0.0-beta.3",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-jsx": "^6.23.0",
"babel-plugin-transform-regenerator": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-register": "^6.26.0",
"css-loader": "^0.28.9",
"cssnano": "^3.4.0",
"eslint": "^3.9.1",
"eslint-config-standard": "^6.2.1",
"eslint-plugin-promise": "^3.3.1",
"eslint-plugin-react": "^7.4.0",
"eslint-plugin-standard": "^2.0.1",
"gulp": "^4.0.0",
"gulp-concat": "^2.4.1",
"gulp-if": "^2.0.0",
"gulp-imagemin": "~2.4.0",
"gulp-postcss": "^6.0.1",
"gulp-rename": "^1.2.2",
"gulp-sass": "^4.0.1",
"gulp-uglify": "~1.5.0",
"html-minify-loader": "~1.1.0",
"html-webpack-plugin": "^2.30.1",
"istanbul": "~0.4.2",
"less": "^3.0.4",
"less-loader": "^4.1.0",
"mocha": "~2.4.5",
"raw-loader": "~0.5.1",
"should": "^8.2.2",
"style-loader": "^0.20.2",
"vinyl-named": "~1.1.0",
"webpack": "^3.11.0",
"webpack-merge": "^4.1.1"
}
}
"name": "hudozka",
"private": true,
"version": "5.0.0",
"description": "",
"main": "src/bin/www.js",
"scripts": {
"start": "NODE_ENV=production node src/server.js",
"dev": "node src/server.js",
"build": "next build && next export -o dist && npm run sitemap && npm run feed && npm run robots-txt",
"robots-txt": "node scripts/robots-txt.js",
"sitemap": "node scripts/sitemap.js",
"feed": "node scripts/feed.js",
"test": "mocha --compilers js:babel-register"
},
"repository": {
"type": "git",
"url": "https://github.com/tmshv/hudozka"
},
"author": "Roman Timashev",
"license": "ISC",
"dependencies": {
"axios": "^0.19.0",
"classnames": "^2.2.5",
"co": "^4.4.0",
"ejson": "^2.1.2",
"handlebars": "^4.1.2",
"immutable": "^3.8.2",
"instagram-node": "^0.5.1",
"koa": "^2.7.0",
"koa-route": "^3.1.0",
"lodash": "^4.17.15",
"markdown": "^0.5.0",
"moment": "^2.22.2",
"mongodb": "^2.1.14",
"mz": "^2.4.0",
"next": "^9.0.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-redux": "^4.4.8",
"react-router": "^4.2.0",
"react-router-redux": "^4.0.8",
"redux": "^3.7.2",
"redux-thunk": "^2.2.0",
"request-promise": "^2.0.1",
"sitemap": "^1.5.0",
"thunkify": "^2.1.2"
},
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.5",
"@babel/node": "^7.4.5",
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/plugin-proposal-object-rest-spread": "^7.4.4",
"@babel/polyfill": "^7.0.0-beta.47",
"@babel/preset-env": "^7.4.5",
"@babel/register": "^7.0.0-beta.47",
"@zeit/next-css": "^1.0.1",
"@zeit/next-sass": "^1.0.1",
"autoprefixer": "^6.3.6",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-react-jsx": "^6.23.0",
"babel-plugin-transform-regenerator": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-register": "^6.26.0",
"cssnano": "^4.1.10",
"eslint": "^3.9.1",
"eslint-config-standard": "^6.2.1",
"eslint-plugin-promise": "^3.3.1",
"eslint-plugin-react": "^7.4.0",
"eslint-plugin-standard": "^2.0.1",
"feed": "^4.0.0",
"mocha": "^6.1.4",
"node-sass": "^4.12.0",
"should": "^8.2.2",
"webpack": "^4.32.2"
}
}
+72
View File
@@ -0,0 +1,72 @@
import React from 'react'
import Document, { Html, Head, Main, NextScript } from 'next/document'
import { YMetrika } from '../src/components/YMetrika'
import { GAnalytics } from '../src/components/GAnalytics'
export default class MyDocument extends Document {
// static async getInitialProps(ctx) {
// const initialProps = await Document.getInitialProps(ctx);
// return { ...initialProps };
// }
render() {
return (
<Html>
<Head>
<style>{`body { margin: 0 } /* custom! */`}</style>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, maximum-scale=1.0" />
<meta name="referrer" content="unsafe-url" />
<meta name="google-site-verification" content="8BxOTw-Q02DS2KAdcGcnIMwiPZ58DVB18XpOfRibCcg" />
<meta name="google-site-verification" content="A-TEemUw4hHpULUDCd6xhxfHnyRbgKtss90JGKXnSpw" />
<meta name='yandex-verification' content='643c7dac144b9dac' />
<meta name="msvalidate.01" content="E878301FE3E3F12D233A6B1156889601" />
<link rel="icon" type="image/png" href="/static/graphics/favicon-32.png" />
<link rel="apple-touch-icon" sizes="57x57" href="/static/graphics/favicon-57.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/static/graphics/favicon-72.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/static/graphics/favicon-114.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/static/graphics/favicon-144.png" />
<link
rel="alternate"
type="application/rss+xml"
title="Шлиссельбургская Детская Художественная Школа"
href="/feed.xml"
/>
{/* Fotorama + jQuery*/}
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js" />
<link href="//cdnjs.cloudflare.com/ajax/libs/fotorama/4.6.4/fotorama.css" rel="stylesheet" />
<script src="//cdnjs.cloudflare.com/ajax/libs/fotorama/4.6.4/fotorama.js" />
{/* Opa */}
<script src="/static/opa.js" />
<link rel="stylesheet" href="/static/opa.css" />
{/* Likely */}
<script src='/static/likely.js' />
<link href='/static/likely.css' rel='stylesheet' />
{/* Fonts */}
<link href="https://fonts.googleapis.com/css?family=PT+Sans|PT+Serif" rel="stylesheet" as="font" />
{/* Instagram Embed */}
<script async defer src="//platform.instagram.com/en_US/embeds.js" />
<YMetrika account={24027460} />
<GAnalytics account={'UA-60209057-1'} />
</Head>
<body className="custom_class">
<Main />
<NextScript />
<script src="/static/app.js" />
</body>
</Html>
);
}
}
+25
View File
@@ -0,0 +1,25 @@
import React from 'react'
import '../src/style/style.scss'
const Page = props => (
<div className="body-wrapper">
<main className="body-wrapper__content">
<section className="error-404">
<div>
<h1>Страница не найдена.</h1>
<img src="/static/graphics/skull.jpg" />
</div>
<div><a href="/">На главную</a></div>
</section>
</main>
<footer className="footer--centered">
<div className="copyright">
<span>©&nbsp;2012—2019 Шлиссельбургская детская художественная школа</span>
</div>
</footer>
</div>
)
export default Page
+44
View File
@@ -0,0 +1,44 @@
import React from 'react'
import Head from 'next/head'
import { App } from '../../src/components/App'
import Article from '../../src/components/Article'
import Html from '../../src/components/Html'
import menuModel from '../../src/models/menu'
import { buildMenu } from '../../src/lib/menu'
import { createApiUrl, requestGet, wrapInitialProps } from '../../src/next-lib'
const Page = (props) => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
menuPadding={true}
>
<Head>
<title>{props.album.title}</title>
</Head>
<div className="content content_thin">
<Article
title={props.album.title}
date={props.album.date}
shareable={true}
>
<Html
html={props.album.post}
/>
</Article>
</div>
</App>
)
Page.getInitialProps = wrapInitialProps(async (ctx) => {
const pageUrl = '/gallery'
const id = ctx.query.album
const album = await requestGet(createApiUrl(ctx.req, `/api/albums/${id}`), {})
return {
album,
pageUrl,
}
})
export default Page
+19
View File
@@ -0,0 +1,19 @@
import { connect } from '../../../src/core/db'
import Album from '../../../src/core/Album'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const id = req.query.album
const album = await Album.findById(id)
if (album) {
res.json(album)
} else {
res.status(404)
res.json({
error: `Album ${id} not found`
})
}
})
+19
View File
@@ -0,0 +1,19 @@
import { connect } from '../../../src/core/db'
import Album from '../../../src/core/Album'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const data = await Album.find({}, { sort: { date: -1 } })
if (data) {
res.json({
items: data,
})
} else {
res.status(404)
res.json({
error: `Article not found`
})
}
})
+20
View File
@@ -0,0 +1,20 @@
import { connect } from '../../../src/core/db'
import Album from '../../../src/core/Album'
export default async (req, res) => {
await connect()
const data = await Album.find({})
if (data) {
const items = data.map(x => `/album/${x.id}`)
res.json({
items
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
+22
View File
@@ -0,0 +1,22 @@
import { connect } from '../../../src/core/db'
import Article from '../../../src/core/Article'
import { encodeArticle } from '../../../src/api/articles'
export default async (req, res) => {
await connect()
const {
query: { article: id }
} = req;
const article = await Article.findById(id)
if (article) {
res.json(encodeArticle(article))
} else {
res.status(404)
res.json({
error: `Article ${id} not found`
})
}
}
+22
View File
@@ -0,0 +1,22 @@
import { connect } from '../../../src/core/db'
import { findArticles } from '../../../src/api/articles'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const { query } = req
const page = parseInt(query.page)
const pageSize = parseInt(query.pageSize)
const data = await findArticles(page, pageSize)
if (data) {
res.json(data)
} else {
res.status(404)
res.json({
error: `Article ${id} not found`
})
}
})
+21
View File
@@ -0,0 +1,21 @@
import { connect } from '../../../src/core/db'
import Article from '../../../src/core/Article'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
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`
})
}
})
+20
View File
@@ -0,0 +1,20 @@
import { connect } from '../../../src/core/db'
import Document from '../../../src/core/Document'
import { encodeFile } from '../../../src/api/files'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const id = req.query.document
const document = await Document.findById(id)
if (document) {
res.json(encodeFile(document))
} else {
res.status(404)
res.json({
error: `Document ${id} not found`
})
}
})
+24
View File
@@ -0,0 +1,24 @@
import { connect } from '../../../src/core/db'
import Document from '../../../src/core/Document'
import { encodeFile } from '../../../src/api/files'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const documents = await Document.find({hidden: false})
if (documents) {
const items = documents
.map(encodeFile)
.filter(x => x.imageUrl) // skip documents without preview image; needs to be refactored
res.json({
items,
})
} else {
res.status(404)
res.json({
error: `Documents not found`
})
}
})
+21
View File
@@ -0,0 +1,21 @@
import { connect } from '../../../src/core/db'
import Document from '../../../src/core/Document'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const data = await Document.find({})
if (data) {
const items = data.map(x => `/document/${x.id}`)
res.json({
items
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
})
+20
View File
@@ -0,0 +1,20 @@
import { connect } from '../../src/core/db'
import Image from '../../src/core/Image'
import { encodeImage } from '../../src/api/image'
import { withMiddleware } from '../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const file = req.query.file
const image = await Image.findByFile(file)
if (image) {
res.json(encodeImage(image))
} else {
res.status(404)
res.json({
error: `Image ${file} not found`
})
}
})
+21
View File
@@ -0,0 +1,21 @@
import { connect } from '../../src/core/db'
import Page from '../../src/core/Page'
import { getPathWithNoTrailingSlash } from '../../src/lib/url'
import { encodePage } from '../../src/api/page'
import { withMiddleware } from '../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const page = getPathWithNoTrailingSlash(req.query.page)
const resource = await Page.findByUrl(page)
if (resource) {
res.json(encodePage(resource))
} else {
res.status(404)
res.json({
error: `Image ${page} not found`
})
}
})
+20
View File
@@ -0,0 +1,20 @@
import { connect } from '../../../src/core/db'
import Page from '../../../src/core/Page'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const items = await Page.find({})
if (items) {
res.json({
items,
})
} else {
res.status(404)
res.json({
error: `Article ${id} not found`
})
}
})
+21
View File
@@ -0,0 +1,21 @@
import { connect } from '../../../src/core/db'
import Page from '../../../src/core/Page'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const data = await Page.find({})
if (data) {
const items = data.map(x => x.url)
res.json({
items,
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
})
+20
View File
@@ -0,0 +1,20 @@
import { connect } from '../../../src/core/db'
import Teacher from '../../../src/core/Teacher'
import { encodePerson } from '../../../src/api/persons'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const id = req.query.person
const data = await Teacher.findById(id)
if (data) {
res.json(encodePerson(data))
} else {
res.status(404)
res.json({
error: `Article ${id} not found`
})
}
})
+33
View File
@@ -0,0 +1,33 @@
import { connect } from '../../../src/core/db'
import Teacher from '../../../src/core/Teacher'
import { prioritySort } from '../../../src/lib/sort'
import { encodePerson } from '../../../src/api/persons'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const order = [
'mg-timasheva',
'va-sarzhin',
'od-gogoleva',
'my-valkova',
'nv-andreeva',
'vv-voronova',
]
const teachersSorted = prioritySort.bind(null, [...order], t => t.id)
const teachers = await Teacher.find({ hidden: false })
const data = teachersSorted(teachers).map(encodePerson)
if (data) {
res.json({
items: data,
})
} else {
res.status(404)
res.json({
error: `Article ${id} not found`
})
}
})
+21
View File
@@ -0,0 +1,21 @@
import { connect } from '../../../src/core/db'
import Teacher from '../../../src/core/Teacher'
import { withMiddleware } from '../../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const data = await Teacher.find({})
if (data) {
const items = data.map(x => `/teacher/${x.id}`)
res.json({
items
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
})
+20
View File
@@ -0,0 +1,20 @@
import { connect } from '../../src/core/db'
import { findSchedule } from '../../src/core/schedule'
import { withMiddleware } from '../../src/middlewares/withMiddleware'
export default withMiddleware(async (req, res) => {
await connect()
const period = req.query.period
const semester = req.query.semester
const data = await findSchedule(period, semester)
if (data) {
res.json(data)
} else {
res.status(404)
res.json({
error: `Schedule ${period}:${semester} not found`
})
}
})
+56
View File
@@ -0,0 +1,56 @@
import React from 'react'
import Head from 'next/head'
import { get } from 'lodash'
import { App } from '../../src/components/App'
import Article from '../../src/components/Article'
import Html from '../../src/components/Html'
import menuModel from '../../src/models/menu'
import { buildMenu } from '../../src/lib/menu'
import { Meta } from '../../src/components/Meta'
import { meta } from '../../src/lib/meta'
import { createApiUrl, requestGet, wrapInitialProps } from '../../src/next-lib'
const Page = (props) => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
menuPadding={true}
>
<Head>
<title>{props.article.title}</title>
<Meta meta={props.meta} />
</Head>
<div className={'content content_thin'}>
<Article
title={props.article.title}
date={props.article.date}
tags={props.article.tags}
shareable={true}
>
<Html
html={props.article.post}
/>
</Article>
</div>
</App>
)
Page.getInitialProps = wrapInitialProps(async (ctx) => {
const pageUrl = '/'
const id = ctx.query.article
const article = await requestGet(createApiUrl(ctx.req, `/api/articles/${id}`), {})
const image = get(article, 'preview.artifacts.fb', {})
return {
article,
pageUrl,
meta: meta({
title: article.title,
image: image.src,
imageWidth: image.width,
imageHeight: image.height,
}),
}
})
export default Page
+53
View File
@@ -0,0 +1,53 @@
import React from 'react'
import Head from 'next/head'
import { App } from '../../src/components/App'
import { ArticleCardList } from '../../src/components/ArticleCardList'
import menuModel from '../../src/models/menu'
import { buildMenu } from '../../src/lib/menu'
import { meta } from '../../src/lib/meta'
import { Meta } from '../../src/components/Meta'
import { createApiUrl, requestGet, wrapInitialProps } from '../../src/next-lib'
const Page = (props) => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
menuPadding={true}
>
<Head>
<title>{props.title}</title>
<Meta meta={props.meta} />
</Head>
<div className="content content_wide">
<ArticleCardList
articles={props.articles}
prevPage={props.prevPage}
nextPage={props.nextPage}
/>
</div>
</App>
)
Page.getInitialProps = wrapInitialProps(async (ctx) => {
const pageUrl = '/'
const page = parseInt(ctx.query.page)
const pageSize = parseInt(ctx.query.pageSize) || 15
const res = await requestGet(createApiUrl(ctx.req, `/api/articles?page=${page}&pageSize=${pageSize}`), {})
const articles = res.items || []
const nextPage = res.nextPage
const prevPage = res.prevPage
const title = 'События'
return {
articles,
nextPage,
prevPage,
pageUrl,
title,
meta: meta({
title,
})
}
})
export default Page
+74
View File
@@ -0,0 +1,74 @@
import React from 'react'
import Head from 'next/head'
import { get } from 'lodash'
import { App } from '../src/components/App'
import { Meta } from '../src/components/Meta'
import { CollectiveImage } from '../src/components/CollectiveImage'
import PersonCard from '../src/components/PersonCard'
import { CardList } from '../src/components/CardList'
import menuModel from '../src/models/menu'
import { buildMenu } from '../src/lib/menu'
import { meta } from '../src/lib/meta'
import { createApiUrl, requestGet, wrapInitialProps } from '../src/next-lib'
const Page = (props) => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
menuPadding={true}
>
<Head>
<title>{props.title}</title>
<Meta meta={props.meta} />
</Head>
<div className="content content_semi-wide">
{!props.image ? null : (
<CollectiveImage
data={props.image}
style={{
marginBottom: 'var(--double-margin)',
}}
/>
)}
<CardList
items={props.persons}
renderItem={(person, index) => (
<PersonCard
key={index}
profile={person}
picture={person.picture}
url={person.url}
name={person.name}
/>
)}
/>
</div>
</App>
)
Page.getInitialProps = wrapInitialProps(async (ctx) => {
const pageUrl = ctx.req.url
const res = await requestGet(createApiUrl(ctx.req, '/api/persons'), {})
const persons = res.items || []
const title = 'Преподаватели Шлиссельбургской ДХШ'
const imageFile = 'Images/HudozkaCollective2017.jpg'
const resImage = await requestGet(createApiUrl(ctx.req, `/api/image?file=${imageFile}`), null)
const image = get(resImage, 'data.artifacts.large', null)
return {
persons,
image,
pageUrl,
title,
meta: meta({
title,
url: pageUrl,
description: 'Преподаватели Шлиссельбургской ДХШ',
})
}
})
export default Page
+43
View File
@@ -0,0 +1,43 @@
import React from 'react'
import Head from 'next/head'
import { App } from '../../src/components/App'
import Document from '../../src/components/Document'
import Share from '../../src/components/Share'
import menuModel from '../../src/models/menu'
import { buildMenu } from '../../src/lib/menu'
import { meta } from '../../src/lib/meta'
import { Meta } from '../../src/components/Meta'
import { createApiUrl, requestGet, wrapInitialProps } from '../../src/next-lib'
const Page = (props) => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
menuPadding={true}
>
<Head>
<title>{props.file.title}</title>
<Meta meta={props.meta} />
</Head>
<div className="content content_wide">
<Document {...props.file} />
<Share />
</div>
</App>
)
Page.getInitialProps = wrapInitialProps(async (ctx) => {
const pageUrl = '/documents'
const id = ctx.query.document
const file = await requestGet(createApiUrl(ctx.req, `/api/files/${id}`), {})
return {
file,
pageUrl,
meta: meta({
title: file.meta,
})
}
})
export default Page
+128
View File
@@ -0,0 +1,128 @@
import React from 'react'
import Head from 'next/head'
import { App } from '../src/components/App'
import { Meta } from '../src/components/Meta'
import menuModel from '../src/models/menu'
import { buildMenu } from '../src/lib/menu'
import { meta } from '../src/lib/meta'
import DocumentList from '../src/components/DocumentList'
import { unique } from '../src/utils/common'
import { createApiUrl, requestGet, wrapInitialProps } from '../src/next-lib'
function getSorted(documents) {
const compose = (...fns) => value => fns
.map(fn => fn)
.reverse()
.reduce((acc, fn) => fn(acc), value)
const pass = fn => value => {
fn(value)
return value
}
const subtract = (a, b) => a - b
const uniqueCategories = unique(i => i.category)
const priority = {
'Основные документы': [
'https://static.shlisselburg.org/art/uploads/ustav-2015.pdf',
'https://static.shlisselburg.org/art/uploads/obrazovatelnaia-litsenziia-2015.pdf',
'https://static.shlisselburg.org/art/uploads/svidetelstvo-na-pomeshchenie.pdf',
'https://static.shlisselburg.org/art/uploads/egriul.pdf',
'https://static.shlisselburg.org/art/uploads/inn.pdf',
'https://static.shlisselburg.org/art/uploads/reshenie-o-sozdanii-shkoly.pdf',
'https://static.shlisselburg.org/art/uploads/kopiia-resheniia-o-naznachenii-rukovoditelia.pdf',
'https://static.shlisselburg.org/art/uploads/postanovlenie-o-sozdanii-munitsipalnogo-obrazovatelnogo-uchrezhdeniia-kultury-detskaia-khudozhestvennaia-shkola.pdf',
],
'Учебные программы': [
'https://static.shlisselburg.org/art/uploads/uchebnaia-programma-zhivopis.pdf',
]
}
const CATEGORY_PRIORITY = [
'Основные документы',
'Планы ФХД',
'Самообследование деятельности',
'Инструкции',
'Образование',
'Учебные программы',
'Платные образовательные услуги',
'Дополнительные общеразвивающие программы',
'Документы для поступления',
]
const sortBy = fn => (a, b) => [a, b]
.map(fn)
.reduce(subtract)
const getPriorityFn = (m, list) => value => list.includes(value)
? list.indexOf(value)
: m
const safeSelect = (d, store, key) => key in store
? store[key]
: d
const categoryPriority = getPriorityFn(10000, CATEGORY_PRIORITY)
const documentPriority = ({ category, fileUrl }) => compose(
getPriorityFn.bind(null, 10000),
safeSelect.bind(null, [], priority)
)(category)(fileUrl)
const documentsOf = category => documents.filter(i => i.category === category)
const sorted = documents => documents.sort(sortBy(documentPriority))
const collectDocumentsByCategory = compose(sorted, documentsOf)
return uniqueCategories(documents)
.sort(sortBy(categoryPriority))
.reduce((acc, name) => [...acc, {
name,
items: collectDocumentsByCategory(name)
}], [])
}
const Page = (props) => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
menuPadding={true}
>
<Head>
<title>{props.title}</title>
<Meta meta={props.meta} />
</Head>
<div className="content content_thin">
{props.collections.map(({ name, items }, index) => (
<DocumentList
key={index}
name={name}
documents={items}
/>
))}
</div>
</App>
)
Page.getInitialProps = wrapInitialProps(async (ctx) => {
const pageUrl = '/documents'
const res = await requestGet(createApiUrl(ctx.req, '/api/files'), {})
const files = res.items || []
const title = 'Документы'
const collections = getSorted(files)
return {
collections,
pageUrl,
title,
meta: meta({
title,
description: 'Документы Шлиссельбургской ДХШ',
})
}
})
export default Page
+102
View File
@@ -0,0 +1,102 @@
import React from 'react'
import Head from 'next/head'
import { App } from '../src/components/App'
// import Album from '../src/components/Album'
import Article from '../src/components/Article'
import { Meta } from '../src/components/Meta'
import menuModel from '../src/models/menu'
import { buildMenu } from '../src/lib/menu'
import { splitBy } from '../src/lib/array'
import { meta } from '../src/lib/meta'
import { createApiUrl, requestGet, wrapInitialProps } from '../src/next-lib'
const albumsByYear = splitBy(album => new Date(album.date).getFullYear())
function getMeta() {
return {
description: 'Галерея работ учащихся Шлиссельбургской Детской Художественной Школы'
}
}
const m = 1
const AlbumImage = ({ data, alt }) => (
<picture>
<img
className="opa"
alt={alt}
src={data.src}
width={data.width * m}
height={data.height * m}
srcSet={(data.set || []).map(({ url, density }) => `${url} ${density}x`)}
/>
</picture>
)
const GItem = ({ album }) => (
<div className="gallery-item">
<a className="invisible" href={album.url}>
<AlbumImage data={album.preview} alt={album.title} />
</a>
</div>
)
const ACollection = ({ title, albums }) => (
<div className="album-collection">
<div className="album-collection__title">{title}</div>
<div className="album-collection__body">{
albums.map((album, index) => (
<GItem key={index} album={album} />
))
}</div>
</div>
)
const Page = (props) => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
menuPadding={true}
>
<Head>
<title>{props.title}</title>
<Meta meta={props.meta} />
</Head>
<div className="content content_semi-wide">
<Article
// title={meta.title}
title={'Галерея'}
>
{props.collections.map(([year, albums], index) => (
<ACollection
key={index}
title={year}
albums={albums}
/>
))
}
</Article>
</div>
</App>
)
Page.getInitialProps = wrapInitialProps(async (ctx) => {
const pageUrl = ctx.req.url
const res = await requestGet(createApiUrl(ctx.req, `/api/albums`), {})
const items = res.items || []
const albumCollections = albumsByYear(items)
const title = 'Галерея'
return {
collections: [...albumCollections.entries()],
pageUrl,
title,
meta: meta({
title,
description: 'Галерея работ учащихся Шлиссельбургской Детской Художественной Школы'
})
}
})
export default Page
+58
View File
@@ -0,0 +1,58 @@
import React from 'react'
import Head from 'next/head'
import { App } from '../src/components/App'
import { Meta } from '../src/components/Meta'
import { ArticleCardList } from '../src/components/ArticleCardList'
import { HudozkaTitle } from '../src/components/HudozkaTitle'
import menuModel from '../src/models/menu'
import { meta } from '../src/lib/meta'
import { buildMenu } from '../src/lib/menu'
import { createApiUrl, requestGet, wrapInitialProps } from '../src/next-lib'
const Page = (props) => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
menuPadding={true}
>
<Head>
<title>{props.title}</title>
<Meta meta={props.meta} />
</Head>
<div className="content content_wide">
<HudozkaTitle />
<ArticleCardList
articles={props.articles}
prevPage={props.prevPage}
nextPage={props.nextPage}
/>
</div>
</App>
)
Page.getInitialProps = wrapInitialProps(async (ctx) => {
const pageUrl = '/'
const page = 1
const pageSize = ctx.query.pageSize || 15
const res = await requestGet(createApiUrl(ctx.req, `/api/articles?page=${page}&pageSize=${pageSize}`), {})
const articles = res.items || []
const nextPage = res.nextPage
const prevPage = res.prevPage
const title = 'Шлиссельбургская ДХШ'
return {
articles,
nextPage,
prevPage,
pageUrl,
title,
meta: meta({
title,
url: pageUrl,
})
}
})
export default Page
+51
View File
@@ -0,0 +1,51 @@
import React from 'react'
import Head from 'next/head'
import { App } from '../src/components/App'
import Page from '../src/components/Page'
import menuModel from '../src/models/menu'
import { buildMenu } from '../src/lib/menu'
import { Meta } from '../src/components/Meta'
import { meta } from '../src/lib/meta'
import { createApiUrl, requestGet, wrapInitialProps } from '../src/next-lib'
const Index = (props) => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
menuPadding={true}
>
<Head>
<title>{props.title}</title>
<Meta meta={props.meta} />
</Head>
<div className={'content content_thin'}>
<Page
shareable={true}
>
{props.content}
</Page>
</div>
</App>
)
Index.getInitialProps = wrapInitialProps(async (ctx) => {
const pageUrl = ctx.req.url
// // const pageUrl = '/neighbors/2018'
const page = await requestGet(createApiUrl(ctx.req, `/api/page?page=${pageUrl}`), {})
const image = page.preview ? page.preview.artifacts.fb : {}
return {
content: page.data,
pageUrl,
title: page.title,
meta: meta({
title: page.title,
image: image.src,
imageWidth: image.width,
imageHeight: image.height,
}),
}
})
export default Index
+43
View File
@@ -0,0 +1,43 @@
import React from 'react'
import Head from 'next/head'
import { App } from '../src/components/App'
import Schedule from '../src/components/Schedule'
import Share from '../src/components/Share'
import menuModel from '../src/models/menu'
import { buildMenu } from '../src/lib/menu'
import { createApiUrl, requestGet, wrapInitialProps } from '../src/next-lib'
const Page = (props) => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
menuPadding={true}
>
<Head>
<title>{props.title}</title>
</Head>
<div className="content content_wide">
{/*<ScheduleList schedules={schedules}/>*/}
<Schedule {...props.schedule} />
<Share />
</div>
</App>
)
Page.getInitialProps = wrapInitialProps(async (ctx) => {
const pageUrl = ctx.req.url
const period = '2016-2017'
const semester = 'spring'
const schedule = await requestGet(createApiUrl(ctx.req, `/api/schedule?period=${period}&semester=${semester}`), {})
return {
schedule,
pageUrl,
title: 'Расписание',
}
})
export default Page
+51
View File
@@ -0,0 +1,51 @@
import React from 'react'
import axios from 'axios'
import Head from 'next/head'
import { App } from '../../src/components/App'
import Page from '../../src/components/Page'
import { Meta } from '../../src/components/Meta'
import menuModel from '../../src/models/menu'
import { buildMenu } from '../../src/lib/menu'
import { meta } from '../../src/lib/meta'
import { createApiUrl, requestGet, wrapInitialProps } from '../../src/next-lib'
const Index = (props) => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
menuPadding={true}
>
<Head>
<title>{props.title}</title>
<Meta meta={props.meta} />
</Head>
<div className="content content_thin">
<Page shareable={true}>
{props.person.post}
</Page>
</div>
</App>
)
Index.getInitialProps = wrapInitialProps(async (ctx) => {
const pageUrl = '/collective'
const id = ctx.query.person
const person = await requestGet(createApiUrl(ctx.req, `/api/persons/${id}`), {})
const name = person.name || []
const title = name.join(' ')
const image = person.preview ? person.preview.artifacts.fb : {}
return {
pageUrl,
person,
title,
meta: meta({
title,
image: image.src,
imageWidth: image.width,
imageHeight: image.height,
})
}
})
export default Index
View File
File renamed without changes.
+43
View File
@@ -0,0 +1,43 @@
const { Feed } = require('feed')
const axios = require('axios')
const fs = require('fs').promises
const api = axios.create({
baseURL: 'http://localhost:3000',
})
async function getArticles(pageSize) {
const page = 1
const res = await api.get(`/api/articles?page=${page}&pageSize=${pageSize}`)
return res.data.items
}
(async () => {
const articles = await getArticles(30)
const url = 'https://art.shlisselburg.org'
const feed = new Feed({
title: 'Шлиссельбургская ДХШ',
id: url,
link: url,
feedLinks: {
atom: 'https://art.shlisselburg.org/feed.xml',
},
});
articles.forEach(article => {
feed.addItem({
title: article.title,
date: new Date(article.date),
id: article.id,
link: article.url,
content: article.post,
})
})
const content = feed.atom1()
await fs.writeFile('dist/feed.xml', content, { encoding: 'utf-8' })
})()
+5
View File
@@ -0,0 +1,5 @@
const fs = require('fs').promises;
(async () => {
await fs.copyFile('public/robots.txt', 'dist/robots.txt')
})()
+80
View File
@@ -0,0 +1,80 @@
const sitemap = require('sitemap')
const axios = require('axios')
const fs = require('fs').promises
const homeUrl = 'https://art.shlisselburg.org'
const sitemapCacheTime = 600000
const api = axios.create({
baseURL: 'http://localhost:3000',
})
async function getMenuUrls(frequency = 'daily') {
const menu = [
'/',
'/schedule',
'/documents',
'/collective',
'/gallery',
]
return menu
// .filter(i => 'url' in i)
.map(url => ({
url,
changefreq: frequency
}))
}
async function getPageUrls(frequency = 'daily') {
const res = await api.get('/api/pages/urls')
return res.data.items.map(url => ({
changefreq: frequency,
url,
}))
}
async function getAlbumsUrls() {
const res = await api.get('/api/albums/urls')
return res.data.items.map(url => ({
changefreq: 'monthly',
url,
}))
}
async function getTeacherUrls() {
const res = await api.get('/api/persons/urls')
return res.data.items.map(url => ({
changefreq: 'monthly',
url,
}))
}
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(),
getAlbumsUrls(),
getTeacherUrls(),
getArticleUrls(),
])
urls = urls.reduce((urls, i) => urls.concat(i))
let map = sitemap.createSitemap({
hostname: homeUrl,
cacheTime: sitemapCacheTime,
urls
})
const content = map.toString()
await fs.writeFile('dist/sitemap.xml', content, { encoding: 'utf-8' })
})()
-3
View File
@@ -1,3 +0,0 @@
{
"presets": ["es2015", "react", "stage-0"]
}
-11
View File
@@ -1,11 +0,0 @@
.DS_Store
.idea
*.sublime*
npm-debug.log
.private*
/node_modules
/build
/out
/coverage
-41
View File
@@ -1,41 +0,0 @@
#!/bin/sh
echo "Deploying Instagram Microservice"
#NODE_ENV=production gulp deploy
server="art.shburg.org"
tarfile="build.tar.gz"
tarpath=$TMPDIR${tarfile}
destpath="~/www/art.shburg.org/services/instagram"
private=".private"
echo "Building..."
NODE_ENV=production npm run webpack
npm run build
echo "Packing..."
env GZIP=-9 tar -czf ${tarpath} \
--disable-copyfile \
--exclude=node_modules \
--exclude=.git \
--exclude=.idea \
--exclude=.DS_Store \
--exclude=.private* \
--exclude=src \
--exclude=deploy.sh \
--exclude=install.sh \
.
echo "Build file: $tarpath"
scp ${tarpath} hoster@${server}:${destpath}
echo "Build file successfully uploaded on ${server}"
scp ${private} hoster@${server}:${destpath}
echo "Installing..."
ssh hoster@${server} "bash -s" -- < ./install.sh ${destpath} ${tarfile}
rm ${tarpath}
echo "Done"
-8
View File
@@ -1,8 +0,0 @@
{
"uid": "hudozka-instagram",
"append": true,
"watch": false,
"script": "index.js",
"sourceDir": "/home/hoster/www/art.shburg.org/services/instagram/out",
"wordingDir": "/home/hoster/www/art.shburg.org/services/instagram"
}
-17
View File
@@ -1,17 +0,0 @@
#!/bin/sh
dir_path="$1"
build_file="$2"
app="hudozka-instagram"
script="forever.json"
cd ${dir_path}
gunzip -c ${build_file} | tar xopf -
npm install --production --loglevel error
if forever restart ${app}; then
echo Instagram Microservice restarted
else
forever start ${script}
echo Instagram Microservice started
fi
-39
View File
@@ -1,39 +0,0 @@
server {
listen 443 ssl;
server_name art.shburg.org;
ssl_certificate /etc/letsencrypt/live/art.shlisselburg.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/art.shlisselburg.org/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers AES256+EECDH:AES256+EDH:!aNULL;
location /service/instagram {
proxy_pass http://hudozka_instagram/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
error_log /var/log/nginx/art.shburg.instagram.error.log;
access_log /var/log/nginx/art.shburg.instagram.access.log;
}
server {
listen 80;
server_name art.shburg.org;
return 301 https://art.shburg.org$request_uri;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream hudozka_instagram {
server localhost:18000;
}
-46
View File
@@ -1,46 +0,0 @@
{
"name": "hudozka-instagram",
"version": "0.0.0",
"private": true,
"scripts": {
"devwebpack": "NODE_ENV=development webpack",
"webpack": "webpack",
"start": "babel-node src/index.js",
"build": "babel src -d out"
},
"dependencies": {
"babel-polyfill": "^6.7.2",
"co": "^4.6.0",
"ejson": "^2.1.2",
"instagram-node": "^0.5.8",
"jsonwebtoken": "^5.7.0",
"kerberos": "0.0.19",
"koa": "^2.0.0-alpha.3",
"koa-bodyparser": "^3.0.0",
"koa-compose": "^3.1.0",
"koa-convert": "^1.2.0",
"koa-logger": "^2.0.0",
"koa-mount": "^2.0.0",
"koa-route": "^3.0.0",
"koa-session-mongo": "^1.1.0",
"koa-session-store": "^1.1.1",
"koa-static": "^3.0.0",
"mongodb": "^2.1.10",
"mz": "^2.3.1",
"react": "^0.14.7",
"react-dom": "^0.14.7",
"react-router": "^2.0.1",
"request-promise": "^2.0.1",
"thunkify": "^2.1.2"
},
"devDependencies": {
"babel-core": "^6.7.2",
"babel-preset-es2015": "^6.6.0",
"babel-preset-stage-0": "^6.5.0",
"babel-cli": "^6.6.5",
"babel-loader": "^6.2.4",
"babel-preset-react": "^6.5.0",
"html-webpack-plugin": "^2.10.0",
"webpack": "^1.12.14"
}
}
-5
View File
@@ -1,5 +0,0 @@
export default class EndPoint{
constructor(request) {
this.request = request;
}
}
-7
View File
@@ -1,7 +0,0 @@
import EndPoint from './EndPoint';
export default class Feed extends EndPoint {
add(post) {
return this.request.request('PUT', '/timeline', post);
}
}
-33
View File
@@ -1,33 +0,0 @@
import {stringify} from 'ejson';
import {EventEmitter} from 'events';
import request from 'request-promise';
import Feed from './Feed';
let json = i => stringify(i);
export default class Service extends EventEmitter{
constructor(host, service, secret) {
super();
this.version = 'v1';
this.host = host;
this.service = service;
this.secret = secret;
this.feed = new Feed(this);
}
request(method, path, data) {
let buildPath = path => `${this.host}/api/${this.version}${path}`;
let options = {
method: method,
uri: buildPath(path),
headers: {
'Content-Type': 'application/ejson',
'X-Service-Name': this.service,
'X-Service-Token': this.secret
},
body: json(data)
};
return request(options);
}
}
-25
View File
@@ -1,25 +0,0 @@
import React from 'react';
import {render} from 'react-dom';
import {Router, Route, browserHistory} from 'react-router';
import App from './components/App';
render(
(
<Router history={browserHistory}>
<Route path="/" component={App}>
</Route>
</Router>
),
document.querySelector('.app-container')
);
// <Route path="login" component={Login}/>
// <Route path="logout" component={Logout}/>
// <Route path="about" component={About}/>
// <Route path="dashboard" component={Dashboard} onEnter={requireAuth}/>
// render(
// <App/>,
// document.querySelector('.app-container')
// );
-18
View File
@@ -1,18 +0,0 @@
import React from 'react';
export default class App extends React.Component{
handleClick(event){
}
render(){
return (
<div>
<h1>Панель управления инстаграмом художки</h1>
<p>Чтобы управлять, вы должны быть <a href="https://instagram.com/hudozka">hudozka</a></p>
<a href="auth" onClick={this.handleClick.bind(this)}>Войти</a>
</div>
)
}
}
-67
View File
@@ -1,67 +0,0 @@
import path from 'path';
import {readFileSync} from 'fs';
const env = process.env['NODE_ENV'] || 'production';
const isProduction = env === 'production';
let privatePath = path.join(
__dirname,
process.env['PRIVATE'] || '../.private'
);
let privateData = JSON.parse(
readFileSync(privatePath, 'utf-8')
);
export const staticPath = path.join(
__dirname,
'../build'
);
export const port = isProduction ? 18000 : 3000;
export const pathPrefix = isProduction ? '/service/instagram' : '';
export const baseHref = isProduction ? '/service/instagram/' : '/';
// export const pathPrefix = '';
export const indexPath = path.join(
__dirname,
// './templates/index.html'
'../build/main.html'
);
// export const indexPath = '../build/main.html';
export const clientId = privateData['clientId'];
export const clientSecret = privateData['clientSecret'];
export const redirectUri = privateData['redirectUri'];
export const serviceHost = isProduction ? 'http://localhost:1800' : 'http://localhost:8080';
export const serviceName = 'instagram';
export const serviceSecret = privateData['serviceSecret'];
export const jwtSecret = privateData['jwtSecret'];
export const sessionSecret = privateData['sessionSecret'];
export const sessionExpirationTime = 60 * 60 * 24 * 2; // two days in seconds
export const authScope = [
'basic',
// 'public_content',
// 'follower_list',
'comments',
// 'relationships',
'likes'
];
export const postType = 'instagram-2';
export const fetchItemsPerUpdateLoop = 1;
export const updateSubscriptionDelay = 1000 * 60 * 60; // one hour in milliseconds
export const updateSubscriptionCallbackUrl = isProduction ? `https://art.shburg.org${pathPrefix}/callback/hudozka` : 'http://dev.tmshv.ru/callback/hudozka';
export const database = {
host: 'localhost',
port: 27017,
db: 'hudozka',
username: privateData['databaseUser'],
password: privateData['databasePassword']
};
const databaseAuth = database.username && database.password ? `${database.username}:${database.password}@` : '';
export const databaseUri = `mongodb://${databaseAuth}${database.host}:${database.port}/${database.db}`;
-48
View File
@@ -1,48 +0,0 @@
import {collection} from './db';
import {jwtSecret} from '../config';
import {sign} from 'jsonwebtoken';
/**
* {
* username: String,
* bio: String,
* website: String,
* profile_picture: String,
* full_name: String,
* id: String
* }
*
* @param accessToken
* @param user
*/
export async function authorizeUser(accessToken, user) {
const provider = 'instagram';
const users = collection('users');
let profile = await users.findOne({
provider: provider,
username: user.username
});
if(profile){
Object.keys(user).forEach(i => {
profile[i] = user[i];
});
profile.accessToken = accessToken;
profile.jwtToken = await createToken(profile);
await users.update({_id: profile['_id']}, profile);
return profile;
}else{
return null;
}
}
async function createToken(user){
return new Promise((resolve, reject) => {
sign(user, jwtSecret, {}, token => {
if (!token) return reject();
resolve(token);
});
});
}
-15
View File
@@ -1,15 +0,0 @@
import {MongoClient} from 'mongodb';
import co from 'co';
export var db;
export function connect(uri){
return co(function*() {
db = yield MongoClient.connect(uri);
return db;
})
}
export function collection(name){
return db.collection(name);
}
-100
View File
@@ -1,100 +0,0 @@
import {collection} from './db';
import {api} from '../instagram';
import {postFromInstagramMedia} from '../models/post';
import Service from '../api/Service';
import {serviceHost, serviceName, serviceSecret, fetchItemsPerUpdateLoop as num} from '../config';
const service = new Service(serviceHost, serviceName, serviceSecret);
var instagramClient;
export default async function update(id, data) {
const updateTimestamp = data.time;
let items = await fetch(updateTimestamp);
return await Promise.all(
items.map(post => push(post))
);
}
export async function fetch(updateTimestamp) {
const instagram = await client();
try {
let data = await instagram.user.mediaRecent(null, 1, updateTimestamp);
return data.medias.map(postFromInstagramMedia);
} catch (e) {
console.log(e);
return [];
}
}
export async function push(data) {
try {
return await service.feed.add(data);
} catch (e) {
console.log('PUSHING ERROR');
console.log(e);
return null;
}
}
export async function clearSubscriptions(callbackUrl) {
const instagram = await client();
return await instagram.subscriptions.removeAll();
}
export async function updateSubscription(callbackUrl) {
const instagram = await client();
let data = await instagram.subscriptions.list();
let activeSubscriptions = data.result;
console.log(`Active subscriptions ${activeSubscriptions}`);
let objectSubscriptions = activeSubscriptions.filter(i => i.object === 'user' && !i.object_id && i.callback_url === callbackUrl)
if (!objectSubscriptions.length) {
// {
// result: {
// object: 'tag',
// object_id: 'hello_lol',
// aspect: 'media',
// callback_url: 'http://dev.tmshv.ru/callback/hudozka',
// type: 'subscription',
// id: '22502218'
// },
// remaining: 4988,
// limit: 5000
// }
// {
// result: {
// object: 'user',
// object_id: null,
// aspect: 'media',
// callback_url: 'http://dev.tmshv.ru/callback/hudozka',
// type: 'subscription',
// id: '22501989'
// },
// remaining: 4995,
// limit: 5000
// }
await instagram.subscriptions.subscribeUser(callbackUrl);
}
}
async function client() {
if (instagramClient) return instagramClient;
const provider = 'instagram';
const users = collection('users');
let profile = await users.findOne({
provider: provider
});
if (profile) {
instagramClient = api(profile.accessToken);
return instagramClient;
}
return null;
}
-56
View File
@@ -1,56 +0,0 @@
import 'babel-polyfill';
import {databaseUri, port, updateSubscriptionDelay, updateSubscriptionCallbackUrl} from './config'
import {connect} from './core/db';
import server from './server';
// import update from './core/update';
import {updateSubscription, clearSubscriptions} from './core/update';
let repeat = time => fn => setInterval(fn, time);
function main(){
async function loop(){
try{
await connect(databaseUri);
}catch(error){
console.log(error);
}
try{
let app = server();
app.listen(port);
console.log(`Start listening ${port}`);
}catch(error){
console.log(error);
}
await clearSubscriptions();
await updateSubscription(updateSubscriptionCallbackUrl);
repeat(updateSubscriptionDelay)(() => {
updateSubscription(updateSubscriptionCallbackUrl);
});
}
loop();
}
main();
// .then(() => {
// return update('hudozka', {
// "changed_aspect": "media",
// "object": "tag",
// "object_id": "love",
// // "time": 1458675620,
// "time": 1458594000,
// "subscription_id": 22487204,
// "data": {}
// })
// .catch(e => {
// console.log(e);
// })
// .then(result => {
// console.log('update done');
// console.log(result);
// });
// })
@@ -1,25 +0,0 @@
import request from 'request-promise';
export default class EndPoint{
constructor(client, token) {
this.client = client;
this.token = token;
}
method(name){
return this.client[name];
}
request(method, path, data=null) {
let buildPath = path => `https://api.instagram.com/v1${path}`;
let options = {
method: method,
uri: buildPath(path),
headers: {}
};
if(data) options.body = data;
return request(options)
.then(i => JSON.parse(i));
}
}
@@ -1,19 +0,0 @@
import Subscription from './Subscription';
import User from './User';
import Media from './Media';
export default class Instagram {
constructor(clientId, clientSecret, accessToken, createApi) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.accessToken = accessToken;
const authenticatedApi = createApi(clientId, clientSecret, accessToken);
const api = createApi(clientId, clientSecret);
//noinspection JSUnresolvedVariable
this.user = new User(authenticatedApi);
this.subscriptions = new Subscription(api);
this.media = new Media(api, accessToken);
}
}
@@ -1,7 +0,0 @@
import EndPoint from './EndPoint';
export default class Media extends EndPoint {
shortname(name){
return this.request('GET', `/media/shortcode/${name}?access_token=${this.token}`);
}
}
@@ -1,78 +0,0 @@
import EndPoint from './EndPoint';
export default class Subscription extends EndPoint {
list() {
const m = this.method('subscriptions');
return new Promise((resolve, reject) => {
m((error, result, remaining, limit) => {
if(error) return reject(error);
resolve({
result: result,
remaining: remaining,
limit: limit
});
});
});
}
subscribeTag(tag, callbackUrl) {
const m = this.method('add_tag_subscription');
return new Promise((resolve, reject) => {
m(tag, callbackUrl, (error, result, remaining, limit) => {
if(error) return reject(error);
resolve({
result: result,
remaining: remaining,
limit: limit
});
});
});
}
subscribeUser(callbackUrl) {
const m = this.method('add_user_subscription');
return new Promise((resolve, reject) => {
m(callbackUrl, (error, result, remaining, limit) => {
if(error) return reject(error);
resolve({
result: result,
remaining: remaining,
limit: limit
});
});
});
}
removeAll(){
const m = this.method('del_subscription');
return new Promise((resolve, reject) => {
m({all: true}, (error, result, remaining, limit) => {
if (error) return reject(error);
resolve({
result: result,
remaining: remaining,
limit: limit
});
});
});
}
remove(id){
// ig.del_subscription({ id: 1 }, function(err, subscriptions, remaining, limit){});
}
// unsubscribe: function *(id) {
// var ig = createClient();
// var sub = thunkify(ig["del_subscription"]);
// try {
// var s = yield sub({id: id});
// return s[0];
// } catch (error) {
// return null;
// }
// },
}
-34
View File
@@ -1,34 +0,0 @@
import EndPoint from './EndPoint';
export default class User extends EndPoint {
mediaRecent(id = null, count = null, minTime = null, maxTime = null) {
function callback(resolve, reject) {
return function (error, medias, pagination, remaining, limit) {
if (error) return reject(error);
resolve({
medias: medias,
pagination: pagination,
remaining: remaining,
limit: limit
});
}
}
const options = {};
if (count) options.count = count;
if (minTime) options.min_timestamp = minTime;
if (maxTime) options.max_timestamp = maxTime;
if (id) {
let m = this.method('user_media_recent');
return new Promise((resolve, reject) => {
m(id, options, callback(resolve, reject));
});
} else {
let m = this.method('user_self_media_recent');
return new Promise((resolve, reject) => {
m(options, callback(resolve, reject));
});
}
}
}
-43
View File
@@ -1,43 +0,0 @@
import {instagram} from 'instagram-node';
import {clientId, clientSecret} from '../config';
import Instagram from './Instagram';
export function api(accessToken){
return new Instagram(clientId, clientSecret, accessToken, createClient);
}
export function createAuthorizationUrl(redirectUri, scope, state) {
let params = {
scope: scope || ['basic']
};
if (state) params.state = state;
let client = createClient(clientId, clientSecret);
return client['get_authorization_url'](redirectUri, params);
}
export function authorize(code, redirectUri) {
let api = createClient(clientId, clientSecret);
let authorize = api['authorize_user'];
return new Promise((resolve, reject) => {
authorize(code, redirectUri, (error, user) => {
if(error){
reject(error);
}else{
resolve(user);
}
});
});
}
function createClient(clientId, clientSecret, accessToken = null) {
const client = instagram();
client.use({
client_id: clientId,
client_secret: clientSecret
});
if (accessToken) client.use({access_token: accessToken});
return client;
}
@@ -1,12 +0,0 @@
import {api} from '../instagram';
export default function () {
return async(ctx, next) => {
var code = ctx.session.accessToken;
if (code) {
ctx.instagram = api(code);
}
await next();
}
}
-69
View File
@@ -1,69 +0,0 @@
import {postType} from '../config';
let date = i => new Date(parseInt(i) * 1000);
/**
* @param i
* {
* "attribution": null,
* "tags": ["люстра", "кукольныйдомик", "shlb_hudozka", "шлиссельбург", "стекло", "выставочноепространство", "художка"],
* "type": "image",
* "location": null,
* "comments": {"count": 0, "data": []},
* "filter": "Normal",
* "created_time": "1458652687",
* "link": "https://www.instagram.com/p/BDQbcm4Jiid/",
* "likes": {
* "count": 138,
* "data": [{
* "username": "marinada29",
* "profile_picture": "https://scontent.cdninstagram.com/t51.2885-19/10958645_950152745008546_618790272_a.jpg",
* "id": "326555798",
* "full_name": "Marina Davidovich 89024808351"
* }]
* },
* "images": {
* "low_resolution": {
* "url": "https://scontent.cdninstagram.com/t51.2885-15/s320x320/e35/12479404_949349891830020_632926748_n.jpg?ig_cache_key=MTIxMTU4OTAxMjkwODAyNjAxMw%3D%3D.2.l",
* "width": 320,
* "height": 320
* },
* "thumbnail": {
* "url": "https://scontent.cdninstagram.com/t51.2885-15/s150x150/e35/c0.134.1080.1080/12816835_959212777507222_1085128603_n.jpg?ig_cache_key=MTIxMTU4OTAxMjkwODAyNjAxMw%3D%3D.2.c",
* "width": 150,
* "height": 150
* },
* "standard_resolution": {
* "url": "https://scontent.cdninstagram.com/t51.2885-15/s640x640/sh0.08/e35/12479404_949349891830020_632926748_n.jpg?ig_cache_key=MTIxMTU4OTAxMjkwODAyNjAxMw%3D%3D.2.l",
* "width": 640,
* "height": 640
* }
* },
* "users_in_photo": [],
* "caption": {
* "created_time": "1458652687",
* "text": "Люстра. В выставочном пространстве \"Кукольный домик\" оформляется одна из ниш. #shlb_hudozka #кукольныйдомик #выставочноепространство #стекло #люстра #художка #шлиссельбург",
* "from": {
* "username": "hudozka",
* "profile_picture": "https://scontent.cdninstagram.com/t51.2885-19/11357496_464997000344800_2124591831_a.jpg",
* "id": "1668537441",
* "full_name": "Шлиссельбургская ДХШ"
* },
* "id": "1211589015567214752"
* },
* "user_has_liked": false,
* "id": "1211589012908026013_1668537441",
* "user": {
* "username": "hudozka",
* "profile_picture": "https://scontent.cdninstagram.com/t51.2885-19/11357496_464997000344800_2124591831_a.jpg",
* "id": "1668537441",
* "full_name": "Шлиссельбургская ДХШ"
* }
* }
*/
export const postFromInstagramMedia = i => ({
data: i,
type: postType,
date: date(i['created_time']),
id: i.id
});
-64
View File
@@ -1,64 +0,0 @@
import {get, url} from './';
import {authorize, createAuthorizationUrl} from '../instagram';
import {redirectUri, authScope} from '../config';
import {authorizeUser} from '../core/auth';
export function auth() {
return get('/auth', async(ctx) => {
const state = url('/');
const i = createAuthorizationUrl(redirectUri, authScope, state);
ctx.redirect(i, state);
});
}
export function authCallback() {
const root = url('/');
return get('/auth/callback', async(ctx) => {
let state = ctx.query.state;
let code = ctx.query.code;
if(!ctx.session.accessToken) {
try{
var client = await authorize(code, redirectUri);
}catch (e){
console.log(e);
}
if (client){
var user = await authorizeUser(client.access_token, client.user);
var accessToken = client.access_token;
}
if(user) ctx.cookies.set('jwt', user.jwtToken);
ctx.status = user ? 200 : 401;
ctx.session.user = user;
ctx.session.accessToken = accessToken;
}
ctx.redirect(state, root);
});
}
export function authOut(redirectUrl = '/') {
const redirectTo = url(redirectUrl);
const root = url('/');
return get('/auth/out', async(ctx) => {
ctx.session = null;
ctx.redirect(redirectTo, root);
});
}
export function ensureAuth(failRedirect = '/') {
const redirectTo = url(failRedirect);
const root = url('/');
return async (ctx, next) => {
if(!ctx.session.user) {
ctx.redirect(redirectTo, root)
}else{
await next();
}
}
}
@@ -1,48 +0,0 @@
import {get, post} from './';
import update from '../core/update';
export function callbackGet() {
return get('/callback/:id', async (ctx, id) => {
// console.log('challenge', id);
if(ctx.query['hub.mode'] == 'subscribe'){
ctx.body = ctx.query['hub.challenge'];
}else{
ctx.status = 400;
}
});
}
export function callbackPost() {
return post('/callback/:id', async (ctx, id) => {
let body = ctx.request.body;
// console.log(id, body);
update(id, body);
ctx.status = 200;
ctx.body = '';
//body example
// {
// "changed_aspect": "media",
// "object": "tag",
// "object_id": "love",
// "time": 1458675620,
// "subscription_id": 22487204,
// "data": {}
// }
// let sign = ctx.header['x-hub-signature'];
//var hmac = crypto.createHmac("sha1", instagram.client_secret);
//hmac.update(body);
//var shasum = hmac.digest("hex");
//console.log("X-Hub-Signature", sign);
//console.log("Hmac", shasum);
//console.log();
//if(shasum.digest("hex") == sign) {
// instagram.update(body);
//}
});
}
-63
View File
@@ -1,63 +0,0 @@
import fs from 'mz/fs';
import compose from 'koa-compose';
import route from 'koa-route';
import {indexPath, pathPrefix} from '../config';
import main from './main';
import {auth, authCallback, authOut} from './auth';
import {list, addTag, addUser} from './subscription';
import {ensureAuth} from './auth';
import {callbackGet, callbackPost} from './callback';
import {shortname} from './media';
export function statelessRouters() {
return compose([
callbackGet(),
callbackPost()
]);
}
export function routers() {
return compose([
// main(),
auth(),
authCallback(),
authOut(),
user(),
list(),
addTag(),
addUser(),
shortname()
]);
}
export const url = (path, root=pathPrefix) => `${root}${path}`;
// export const url = path => path;
export const get = (path, fn) => route['get'](url(path, ''), fn);
export const post = (path, fn) => route['post'](url(path, ''), fn);
export function index(fn){
return async (ctx) => {
try{
ctx.type = 'text/html';
ctx.body = await fs.createReadStream(indexPath);
}catch(e){
console.log(e);
}
}
}
function user(){
return compose([
ensureAuth(),
get('/user', async (ctx) => {
const user = ctx.session.user.id;
const instagram = ctx.instagram;
let data = await instagram.user.mediaRecent(user);
console.log(data);
ctx.body = data;
})
]);
}
-7
View File
@@ -1,7 +0,0 @@
import {index, get} from './';
export default function () {
return get('/', async (ctx) => {
await index()(ctx);
});
}
-23
View File
@@ -1,23 +0,0 @@
import compose from 'koa-compose';
import {get} from './';
import {ensureAuth} from './auth';
import {push} from '../core/update';
import {postFromInstagramMedia} from '../models/post';
export function shortname() {
return compose([
ensureAuth(),
get('/media/shortname/:name', async(ctx, name) => {
const instagram = ctx.instagram;
let result = await instagram.media.shortname(name);
if(result.meta.code === 200) {
let post = postFromInstagramMedia(result.data);
console.log('SHORTNAME');
ctx.set('Content-Type', 'application/json');
ctx.body = await push(post);
}
})
]);
}
@@ -1,56 +0,0 @@
import compose from 'koa-compose';
import {get} from './';
import {ensureAuth} from './auth';
export function list() {
return compose([
ensureAuth(),
get('/subscriptions', async(ctx) => {
const instagram = ctx.instagram;
ctx.set('Content-Type', 'application/json');
ctx.body = await instagram.subscriptions.list();
})
]);
}
export function addTag() {
return compose([
ensureAuth(),
get('/subscriptions/add-tag/:tag', async(ctx, tag) => {
let callbackUrl = ctx.query.url;
let result = await ctx.instagram.subscriptions.subscribeTag(tag, callbackUrl);
if (result) {
ctx.body = result;
} else {
ctx.status = 500;
}
})
]);
}
export function addUser() {
return compose([
ensureAuth(),
get('/subscriptions/add-user', async (ctx) => {
let callbackUrl = ctx.query.url;
let result = await ctx.instagram.subscriptions.subscribeUser(callbackUrl);
if (result) {
ctx.body = result;
} else {
ctx.status = 500;
}
})
])
}
// export function remove() {
// return get('/subscriptions/remove/:id', async (ctx, id) => {
// let result = yield instagram.api.unsubscribe(id);
// if(result) {
// ctx.body = result;
// }else{
// ctx.status = 500;
// }
// });
// }
-53
View File
@@ -1,53 +0,0 @@
import Koa from 'koa';
import mount from 'koa-mount';
import serve from 'koa-static';
import logger from 'koa-logger';
import convert from 'koa-convert';
import sessionStore from 'koa-session-store';
import store from 'koa-session-mongo';
import bodyParser from 'koa-bodyparser';
import {db} from './core/db';
import instagram from './middlewares/instagram';
import {routers, statelessRouters} from './routers';
import {pathPrefix, staticPath, sessionSecret, sessionExpirationTime} from './config';
export default function(){
const app = new Koa();
app.proxy = true;
app.keys = [sessionSecret];
app.use(logger());
app.use(serve(staticPath));
app.use(bodyParser());
app.use(statelessRouters());
app.use(session('sid', sessionExpirationTime));
app.use(instagram());
app.use(routers());
app.use(error());
return app;
// const root = new Koa();
// root.use(mount(pathPrefix, app));
// return root;
}
function session(name, exp){
return convert(sessionStore({
name: name,
expirationTime: exp,
store: store.create({
db: db,
collection: 'sessions'
})
}))
}
function error(){
return ctx => {
ctx.status = 404;
ctx.body = `Document ${ctx.path} not found`;
};
}
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Instagram Control</title>
<base href="<%= htmlWebpackPlugin.options.baseHref %>">
</head>
<body>
<div class="app-container"></div>
</body>
</html>
@@ -1,54 +0,0 @@
import webpack from'webpack';
import HTML from 'html-webpack-plugin';
const env = process.env['NODE_ENV'] || 'production';
const isProduction = env === 'production';
// import autoprefixer from 'autoprefixer';
import {baseHref} from './src/config';
module.exports = {
entry: './src/app.js',
watch: !isProduction,
//resolve: {
// modulesDirectories: [
// './node_modules'
// ]
//},
output: {
path: './build',
filename: 'app.js'
},
// devServer: {
// historyApiFallback: true,
// hot: true,
// inline: true,
// progress: true,
// host: 'localhost',
// port: 3000
// },
plugins: [
// new webpack.HotModuleReplacementPlugin(),
new HTML({
filename: 'index.html',
template: 'src/templates/index.ejs',
inject: 'body',
baseHref: baseHref
})
],
module: {
loaders: [
{
test: /\.js$/,
// exclude: /node_modules/,
//exclude: /(node_modules|bower_components)/,
loader: 'babel',
query: {
// presets: ['react'],
cacheDirectory: true
}
}
]
}
};
+71
View File
@@ -0,0 +1,71 @@
import Article from '../core/Article'
import ImageArtifactType from '../core/ImageArtifactType'
import { sortBy } from '../utils/sort'
import { timestamp } from '../lib/date'
import { encodeImage } from './image'
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, pageSize) {
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 prevPage = page > 1
? page - 1
: null
const nextPage = page < totalPages
? page + 1
: null
const items = [
...pinnedArticles.sort(sortArticleByDate),
...articles
]
.map(x => x.plain())
.map(x => ({
...x,
preview: !x.preview ? null : (
x.preview.getPicture(ImageArtifactType.MEDIUM)
)
}))
return {
items,
prevPage,
nextPage,
totalPages,
}
}
export function encodeArticle(article) {
return {
...article,
preview: encodeImage(article.preview),
}
}
+36
View File
@@ -0,0 +1,36 @@
import ImageArtifactType from '../core/ImageArtifactType'
const sizes = [
ImageArtifactType.MEDIUM,
ImageArtifactType.BIG,
ImageArtifactType.ORIGIN,
]
export function encodeFilePreview(file, sizes) {
const preview = file.preview
if (!preview) return null
const image = preview.findArtifact(sizes)
if (!image) return null
return image.url
}
export function encodeFile(file) {
const imageUrl = encodeFilePreview(file, sizes)
const category = (file.category || '')
.replace('Documents/', '')
.replace('Pages/', '')
.replace('Albums/', '')
.replace('Articles /', '')
return {
imageUrl,
category,
title: file.title,
fileName: file.fileInfo.name,
fileSize: file.fileInfo.size,
fileUrl: file.url,
url: file.viewUrl,
}
}
+24
View File
@@ -0,0 +1,24 @@
import ImageArtifactType from '../core/ImageArtifactType'
export function encodeImage(image) {
const artifacts = [
ImageArtifactType.SMALL,
ImageArtifactType.MEDIUM,
ImageArtifactType.BIG,
ImageArtifactType.LARGE,
ImageArtifactType.ORIGIN,
ImageArtifactType.ORIGINAL,
ImageArtifactType.FACEBOOK = 'fb'
].reduce((acc, size) => {
return {
...acc,
[size]: image.getPicture(size),
}
}, {})
return {
artifacts,
hash: image.hash,
file: image.file,
}
}
+11
View File
@@ -0,0 +1,11 @@
import { encodeImage } from './image'
export function encodePage(page) {
return {
id: page.id,
title: page.title,
url: page.url,
data: page.data,
preview: encodeImage(page.preview),
}
}
+13
View File
@@ -0,0 +1,13 @@
import ImageArtifactType from '../core/ImageArtifactType'
import { encodeImage } from './image'
const profilePicture = profile => profile.picture.getPicture(ImageArtifactType.BIG)
export function encodePerson(person) {
return {
...person,
shortName: person.shortName(),
name: person.splitName(),
picture: profilePicture(person),
preview: encodeImage(person.picture),
}
}
+7 -7
View File
@@ -1,9 +1,11 @@
const React = require('react')
const Menu = require('./Menu')
const Footer = require('./Footer')
const Comments = require('./Comments')
import React from 'react'
import Menu from './Menu'
import Footer from './Footer'
import Comments from './Comments'
const App = ({children, menu, showAuthor, menuPadding}) => (
import '../style/style.scss'
export const App = ({children, menu, showAuthor, menuPadding}) => (
<div className="body-wrapper theme-default">
<header>
<div className="navigation">
@@ -28,5 +30,3 @@ const App = ({children, menu, showAuthor, menuPadding}) => (
/>
</div>
)
module.exports = App
+44 -44
View File
@@ -1,60 +1,60 @@
const React = require('react')
const dateFormat = require('../lib/date').dateFormat
import React from 'react'
import { dateFormat } from '../lib/date'
const Tag = ({children}) => (
<li className="ArticleTags-Item">
{children}
</li>
const Tag = ({ children }) => (
<li className="ArticleTags-Item">
{children}
</li>
)
const TagList = ({tags}) => (
<ul className="ArticleTags">
{tags.map((x, i) => (
<Tag key={i}># {x.name}</Tag>
))}
</ul>
const TagList = ({ tags }) => (
<ul className="ArticleTags">
{tags.map((x, i) => (
<Tag key={i}># {x.name}</Tag>
))}
</ul>
)
const Head = ({date, tags = [], children}) => (
<header className="Article-Head">
<h1>{children}</h1>
const Head = ({ date, tags = [], children }) => (
<header className="Article-Head">
<h1>{children}</h1>
{!date ? null : (
<p className="date">{dateFormat(date)}</p>
)}
{!date ? null : (
<p className="date">{dateFormat(date)}</p>
)}
{!(tags.length) ? null : (
<TagList tags={tags}/>
)}
</header>
{!(tags.length) ? null : (
<TagList tags={tags} />
)}
</header>
)
const Share = () => (
<div className="likely">
<div className="vkontakte">Поделиться</div>
<div className="facebook">Поделиться</div>
<div className="telegram">Отправить</div>
<div className="twitter">Твитнуть</div>
</div>
<div className="likely">
<div className="vkontakte">Поделиться</div>
<div className="facebook">Поделиться</div>
<div className="telegram">Отправить</div>
<div className="twitter">Твитнуть</div>
</div>
)
const Article = ({children, date, tags, title, shareable}) => (
<article className="Article">
<Head
date={date}
tags={tags}
>
{title}
</Head>
const Article = ({ children, date, tags, title, shareable }) => (
<article className="Article">
<Head
date={date}
tags={tags}
>
{title}
</Head>
<div className="Article-Body">
{children}
</div>
<div className="Article-Body">
{children}
</div>
{!shareable ? null : (
<Share/>
)}
</article>
{!shareable ? null : (
<Share />
)}
</article>
)
module.exports = Article
export default Article
-35
View File
@@ -1,35 +0,0 @@
const React = require('react')
const dateFormat = require('../lib/date').dateFormat
const Image = require('./Image')
const Date = ({children}) => (
<p className="date">{dateFormat(children)}</p>
)
const Link = ({href, children}) => (
<a className="invisible" href={href}>
{children}
</a>
)
const ArticleCard = ({article}) => (
<article className="ArticleCard">
<Link href={article.url}>
<div className="ArticleCard-image">
{!article.preview ? null : (
<Image data={article.preview} alt={article.title}/>
)}
</div>
<div className="ArticleCard-body">
{article.title}
{!article.date ? null : (
<Date>{article.date}</Date>
)}
</div>
</Link>
</article>
)
module.exports = ArticleCard
+122
View File
@@ -0,0 +1,122 @@
import React from 'react'
import { Image } from './Image'
import { dateFormat } from '../lib/date'
const Date = ({ children }) => (
<p className="date">{dateFormat(children)}</p>
)
export const ArticleCard = ({ article }) => (
<article className="ArticleCard">
<style jsx>{`
.ArticleCard {
--article-card-width: 30%;
--article-card-image-height: 15em;
--article-card-margin: var(--double-margin) var(--single-margin);
display: flex;
flex-direction: column;
margin: var(--article-card-margin);
width: var(--article-card-width);
min-width: 10em;
max-width: 30em;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
overflow: hidden;
}
.ArticleCard > a {
display: block;
position: relative;
}
.ArticleCard-image {
overflow: hidden;
position: relative;
width: 100%;
height: var(--article-card-image-height);
}
.ArticleCard-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: all 150ms ease-out;
position: absolute;
top: 0;
left: 0;
}
.ArticleCard-body {
z-index: 1000;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
height: 5em;
padding: .75em 1em;
}
.ArticleCard .date {
color: #ccc;
font-size: small;
}
.ArticleCard:hover img {
width: 105%;
height: 105%;
top: -2.5%;
left: -2.5%;
}
// Tablet
@media screen and (min-width: var(--tablet-min-width)) and (max-width: var(--tablet-max-width)) {
.ArticleCard {
--article-card-image-height: 12em;
}
@media screen and (orientation: portrait) {
}
}
// Mobile
@media (max-width: var(--mobile-width)) {
.ArticleCard {
--article-card-width: 100%;
--article-card-margin: var(--single-margin) 0;
}
}
`}</style>
<a className="invisible" href={article.url}>
<div className="ArticleCard-image">
{!article.preview ? null : (
<Image
data={article.preview}
alt={article.title}
opa={false}
/>
)}
</div>
<div className="ArticleCard-body">
{article.title}
{!article.date ? null : (
<Date>{article.date}</Date>
)}
</div>
</a>
</article>
)
-26
View File
@@ -1,26 +0,0 @@
const React = require('react')
const Paginator = require('./Paginator')
const ArticleCard = require('./ArticleCard')
const ArticleCardList = ({articles, nextPage, prevPage}) => (
<div className="ArticleCardList">
{!prevPage ? null : (
<Paginator url={`/articles/${prevPage}`} type="top"/>
)}
<div className="ArticleCardList-body">
{articles.map((article, i) => (
<ArticleCard
key={i}
article={article}
/>
))}
</div>
{!nextPage ? null : (
<Paginator url={`/articles/${nextPage}`} type="bottom"/>
)}
</div>
)
module.exports = ArticleCardList
+24
View File
@@ -0,0 +1,24 @@
import React from 'react'
import Paginator from './Paginator'
import { ArticleCard } from './ArticleCard'
export const ArticleCardList = ({ articles, nextPage, prevPage }) => (
<div className="ArticleCardList">
{!prevPage ? null : (
<Paginator url={`/articles/${prevPage}`} type="top" />
)}
<div className="ArticleCardList-body">
{articles.map((article, i) => (
<ArticleCard
key={i}
article={article}
/>
))}
</div>
{!nextPage ? null : (
<Paginator url={`/articles/${nextPage}`} type="bottom" />
)}
</div>
)
+19
View File
@@ -0,0 +1,19 @@
import React from 'react'
export const CardList = props => (
<div className="CardList">
<style jsx>{`
.CardList {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
`}</style>
{props.items.map(
(item, index) => props.renderItem(item, index)
)}
</div>
)
-8
View File
@@ -1,8 +0,0 @@
const React = require('react')
const Image = require('./Image')
module.exports = ({data}) => (
<div className="CollectiveImage">
<Image data={data}/>
</div>
)
Loaded 100 of 220 files, more files were not shown because too many files have changed in this diff. Show more